diff --git a/android/app/build.gradle b/android/app/build.gradle index cf2069f3..d45a3de1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -41,6 +41,7 @@ android { targetSdkVersion 35 versionCode flutterVersionCode.toInteger() versionName flutterVersionName + multiDexEnabled true } signingConfigs { release { @@ -83,6 +84,7 @@ android { namespace "org.fsociety.vernet" compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } @@ -91,6 +93,12 @@ android { } } +dependencies { + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2' + implementation 'androidx.window:window:1.0.0' + implementation 'androidx.window:window-java:1.0.0' +} + flutter { source '../..' } \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d921e5a1..10557b2a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,8 @@ + + diff --git a/android/app/src/main/res/drawable/app_icon.png b/android/app/src/main/res/drawable/app_icon.png new file mode 100644 index 00000000..f6574bba Binary files /dev/null and b/android/app/src/main/res/drawable/app_icon.png differ diff --git a/assets/app_icon.png b/assets/app_icon.png new file mode 100644 index 00000000..f6574bba Binary files /dev/null and b/assets/app_icon.png differ diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 366ef86c..7c2e90f7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -6,8 +6,16 @@ PODS: - Flutter - flutter_isolate (0.0.1): - Flutter + - flutter_local_notifications (0.0.1): + - Flutter - flutter_native_splash (0.0.1): - Flutter + - flutter_timezone (0.0.1): + - Flutter + - in_app_review (0.2.0): + - Flutter + - isar_flutter_libs (1.0.0): + - Flutter - network_info_plus (0.0.1): - Flutter - nsd_ios (0.0.1): @@ -30,7 +38,11 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_icmp_ping (from `.symlinks/plugins/flutter_icmp_ping/ios`) - flutter_isolate (from `.symlinks/plugins/flutter_isolate/ios`) + - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) + - flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`) + - in_app_review (from `.symlinks/plugins/in_app_review/ios`) + - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`) - network_info_plus (from `.symlinks/plugins/network_info_plus/ios`) - nsd_ios (from `.symlinks/plugins/nsd_ios/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) @@ -48,8 +60,16 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_icmp_ping/ios" flutter_isolate: :path: ".symlinks/plugins/flutter_isolate/ios" + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" + flutter_timezone: + :path: ".symlinks/plugins/flutter_timezone/ios" + in_app_review: + :path: ".symlinks/plugins/in_app_review/ios" + isar_flutter_libs: + :path: ".symlinks/plugins/isar_flutter_libs/ios" network_info_plus: :path: ".symlinks/plugins/network_info_plus/ios" nsd_ios: @@ -70,14 +90,18 @@ SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_icmp_ping: 2b159955eee0c487c766ad83fec224ae35e7c935 flutter_isolate: 0edf5081826d071adf21759d1eb10ff5c24503b5 + flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 + flutter_timezone: ffb07bdad3c6276af8dada0f11978d8a1f8a20bb + in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d + isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 network_info_plus: 6d0c3eb8367b8164fa3fb0c19875e3f59d49697f nsd_ios: 8c37babdc6538e3350dbed3a52674d2edde98173 package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 - shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 - url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 70693e4a..a527abb7 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,12 +1,15 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate + } GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/lib/main.dart b/lib/main.dart index f7e72c56..4ee460ef 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; -import 'package:network_info_plus/network_info_plus.dart'; import 'package:network_tools_flutter/network_tools_flutter.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; @@ -9,10 +8,11 @@ import 'package:vernet/helper/app_settings.dart'; import 'package:vernet/helper/consent_loader.dart'; import 'package:vernet/injection.dart'; import 'package:vernet/pages/home_page.dart'; +import 'package:vernet/pages/host_scan_page/host_scan_page.dart'; import 'package:vernet/pages/location_consent_page.dart'; import 'package:vernet/pages/settings_page.dart'; import 'package:vernet/providers/dark_theme_provider.dart'; -import 'package:vernet/services/impls/device_scanner_service.dart'; +import 'package:vernet/repository/notification_service.dart'; AppSettings appSettings = AppSettings.instance; Future main() async { @@ -27,12 +27,17 @@ Future main() async { final bool allowed = await ConsentLoader.isConsentPageShown(); await appSettings.load(); + await NotificationService.initNotification(); + runApp(MyApp(allowed)); FlutterNativeSplash.remove(); } class MyApp extends StatefulWidget { const MyApp(this.allowed, {super.key}); + static final GlobalKey navigatorKey = + GlobalKey(); + // static const Color mainColor = Colors.deepPurple; final bool allowed; @@ -46,21 +51,8 @@ class _MyAppState extends State { @override void initState() { super.initState(); + NotificationService.grantPermissions(); getCurrentAppTheme(); - startScanOnStartup(); - } - - Future startScanOnStartup() async { - if (appSettings.runScanOnStartup) { - final ip = await NetworkInfo().getWifiIP(); - final gatewayIp = appSettings.customSubnet.isNotEmpty - ? appSettings.customSubnet - : await NetworkInfo().getWifiGatewayIP(); - final subnet = gatewayIp!.substring(0, gatewayIp.lastIndexOf('.')); - getIt() - .startNewScan(subnet, ip!, gatewayIp) - .listen((device) {}); - } } Future getCurrentAppTheme() async { @@ -77,18 +69,40 @@ class _MyAppState extends State { child: Consumer( builder: (BuildContext context, value, Widget? child) { return MaterialApp( + navigatorKey: MyApp.navigatorKey, + initialRoute: '/', + onGenerateRoute: (settings) { + switch (settings.name) { + case '/': + return MaterialPageRoute( + builder: (context) => homePage, + ); + + case '/hostscan': + return MaterialPageRoute( + builder: (context) { + return HostScanPage(); + }, + ); + + default: + assert(false, 'Page ${settings.name} not found'); + return null; + } + }, title: 'Vernet', theme: themeChangeProvider.darkTheme ? ThemeData.dark() : ThemeData.light(), - home: widget.allowed - ? const TabBarPage() - : const LocationConsentPage(), + home: homePage, ); }, ), ); } + + Widget get homePage => + widget.allowed ? const TabBarPage() : const LocationConsentPage(); } class TabBarPage extends StatefulWidget { diff --git a/lib/models/wifi_info.dart b/lib/models/wifi_info.dart index 966504d3..ba0c583e 100644 --- a/lib/models/wifi_info.dart +++ b/lib/models/wifi_info.dart @@ -1,5 +1,12 @@ class WifiInfo { - WifiInfo(this._ip, this._bssid, this._name, this.unknown); + WifiInfo( + this._ip, + this._bssid, + this._name, + this.unknown, + this.gatewayIp, + this.isLocationOn, + ); static Set defaultBSSID = {'00:00:00:00:00:00'}; final String? _bssid; @@ -7,6 +14,10 @@ class WifiInfo { final String? _name; bool unknown; String get ip => _ip ?? 'x.x.x.x'; + int totalDevices = 0; + final String gatewayIp; + final bool isLocationOn; + String get subnet => gatewayIp.substring(0, gatewayIp.lastIndexOf('.')); static const String noWifiName = 'Wi-Fi'; diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 86f3953e..31b37544 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -5,7 +5,9 @@ import 'package:network_info_plus/network_info_plus.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:vernet/api/isp_loader.dart'; import 'package:vernet/helper/utils_helper.dart'; +import 'package:vernet/injection.dart'; import 'package:vernet/main.dart'; +import 'package:vernet/models/isar/device.dart'; import 'package:vernet/models/wifi_info.dart'; import 'package:vernet/pages/dns/dns_page.dart'; import 'package:vernet/pages/dns/reverse_dns_page.dart'; @@ -13,8 +15,11 @@ import 'package:vernet/pages/host_scan_page/host_scan_page.dart'; import 'package:vernet/pages/network_troubleshoot/port_scan_page.dart'; import 'package:vernet/pages/ping_page/ping_page.dart'; import 'package:vernet/providers/internet_provider.dart'; +import 'package:vernet/repository/notification_service.dart'; +import 'package:vernet/services/impls/device_scanner_service.dart'; import 'package:vernet/ui/adaptive/adaptive_list.dart'; import 'package:vernet/ui/custom_tile.dart'; +import 'package:vernet/values/strings.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -25,9 +30,13 @@ class HomePage extends StatefulWidget { class _WifiDetailState extends State { WifiInfo? _wifiInfo; - bool _location = false; + bool scanRunning = false; + Set devices = {}; - Future _getWifiInfo() async { + Future _getWifiInfo() async { + if (_wifiInfo != null) { + return _wifiInfo; + } if (Platform.isAndroid) { await Permission.location.request(); } @@ -35,17 +44,39 @@ class _WifiDetailState extends State { final wifiIP = await NetworkInfo().getWifiIP(); final wifiBSSID = await NetworkInfo().getWifiBSSID(); final wifiName = await NetworkInfo().getWifiName(); + final gatewayIp = appSettings.customSubnet.isNotEmpty + ? appSettings.customSubnet + : await NetworkInfo().getWifiGatewayIP() ?? ''; + final bool isLocationOn = (Platform.isAndroid || Platform.isIOS) && + await Permission.location.serviceStatus.isEnabled; + _wifiInfo = WifiInfo( + wifiIP, + wifiBSSID, + wifiName, + wifiName == null, + gatewayIp, + isLocationOn, + ); - setState(() { - _wifiInfo = WifiInfo(wifiIP, wifiBSSID, wifiName, wifiName == null); - }); - if (Platform.isAndroid || Platform.isIOS) { - Permission.location.serviceStatus.isEnabled.then( - (value) => setState(() { - _location = value; - }), - ); + if (appSettings.runScanOnStartup) { + getIt() + .startNewScan(_wifiInfo!.subnet, wifiIP!, gatewayIp) + .listen((device) { + if (mounted) { + setState(() { + scanRunning = true; + devices.add(device); + }); + } + }).onDone(() async { + if (mounted) { + scanRunning = false; + } + await NotificationService.showNotificationWithActions(); + }); } + + return _wifiInfo; } @override @@ -54,25 +85,50 @@ class _WifiDetailState extends State { _getWifiInfo(); } + Widget _getDeviceCountWidget() { + if (appSettings.runScanOnStartup) { + return Row( + children: [ + Text( + '${devices.length} devices ${scanRunning ? 'found' : 'connected'}', + ), + const SizedBox( + width: 8, + ), + if (scanRunning) + const CircularProgressIndicator.adaptive() + else + const SizedBox(), + ], + ); + } + return const SizedBox(); + } + @override Widget build(BuildContext context) { return SingleChildScrollView( child: Column( children: [ Card( - //todo: replace this widget by future builder - child: _wifiInfo == null - ? const CircularProgressIndicator.adaptive() - : AdaptiveListTile( + child: FutureBuilder( + future: _getWifiInfo(), + builder: ( + BuildContext context, + AsyncSnapshot snapshot, + ) { + if (snapshot.hasData && snapshot.data != null) { + final wifiInfo = snapshot.data; + return AdaptiveListTile( minVerticalPadding: 10, leading: const Icon(Icons.router), - title: Text(_wifiInfo!.name), + title: Text(wifiInfo!.name), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Connected to ${_wifiInfo!.bssid}'), + Text('Connected to ${wifiInfo.bssid}'), const SizedBox(height: 5), - if (_location) + if (wifiInfo.isLocationOn) const SizedBox() else Text( @@ -87,16 +143,24 @@ class _WifiDetailState extends State { ), const Divider(height: 3), const SizedBox(height: 10), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => HostScanPage(), - ), - ); - }, - child: const Text('Scan for devices'), + Row( + children: [ + _getDeviceCountWidget(), + const SizedBox( + width: 4, + ), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => HostScanPage(), + ), + ); + }, + child: const Text(StringValue.hostScanPageTitle), + ), + ], ), ], ), @@ -106,7 +170,14 @@ class _WifiDetailState extends State { _getWifiInfo(); }, ), - ), + ); + } else if (snapshot.hasError) { + return const Text("Unable to fetch WiFi details"); + } else { + return const CircularProgressIndicator.adaptive(); + } + }, + ), ), Card( child: AdaptiveListTile( diff --git a/lib/pages/host_scan_page/host_scan_bloc/host_scan_bloc.dart b/lib/pages/host_scan_page/host_scan_bloc/host_scan_bloc.dart index a59629d0..cf35cecc 100644 --- a/lib/pages/host_scan_page/host_scan_bloc/host_scan_bloc.dart +++ b/lib/pages/host_scan_page/host_scan_bloc/host_scan_bloc.dart @@ -12,6 +12,7 @@ import 'package:vernet/main.dart'; import 'package:vernet/models/device_in_the_network.dart'; import 'package:vernet/models/isar/device.dart'; import 'package:vernet/models/isar/scan.dart'; +import 'package:vernet/repository/notification_service.dart'; import 'package:vernet/repository/scan_repository.dart'; import 'package:vernet/services/impls/device_scanner_service.dart'; @@ -75,6 +76,7 @@ class HostScanBloc extends Bloc { emit(HostScanState.foundNewDevice(devices)); } + await NotificationService.showNotificationWithActions(); emit(HostScanState.loadSuccess(devices)); } @@ -99,6 +101,7 @@ class HostScanBloc extends Bloc { final scan = scanList.first; if (scan.onGoing == false) { emit(HostScanState.loadSuccess(devicesSet)); + await NotificationService.showNotificationWithActions(); break; } } diff --git a/lib/pages/host_scan_page/host_scan_page.dart b/lib/pages/host_scan_page/host_scan_page.dart index e40b6c58..2a7e7440 100644 --- a/lib/pages/host_scan_page/host_scan_page.dart +++ b/lib/pages/host_scan_page/host_scan_page.dart @@ -3,13 +3,14 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:vernet/injection.dart'; import 'package:vernet/pages/host_scan_page/host_scan_bloc/host_scan_bloc.dart'; import 'package:vernet/pages/host_scan_page/widgets/host_scan_widget.dart'; +import 'package:vernet/values/strings.dart'; class HostScanPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Scan for Devices'), + title: const Text(StringValue.hostScanPageTitle), ), body: BlocProvider( create: (context) => diff --git a/lib/repository/device_repository.dart b/lib/repository/device_repository.dart index 1b9d08da..bfc9d2ba 100644 --- a/lib/repository/device_repository.dart +++ b/lib/repository/device_repository.dart @@ -49,4 +49,9 @@ class DeviceRepository extends IsarRepository { .build() .watch(fireImmediately: true); } + + Future countByScanId(int scanId) async { + final deviceDB = await _database.open(); + return deviceDB!.devices.filter().scanIdEqualTo(scanId).count(); + } } diff --git a/lib/repository/notification_service.dart b/lib/repository/notification_service.dart new file mode 100644 index 00000000..48abb005 --- /dev/null +++ b/lib/repository/notification_service.dart @@ -0,0 +1,238 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_timezone/flutter_timezone.dart'; +import 'package:timezone/data/latest_all.dart' as tz; +import 'package:timezone/timezone.dart' as tz; + +class ReceivedNotification { + ReceivedNotification({ + required this.id, + required this.title, + required this.body, + required this.payload, + }); + + final int id; + final String? title; + final String? body; + final String? payload; +} + +class NotificationService { + static int id = 1; + static final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + + /// Defines a iOS/MacOS notification category for text input actions. + static const String darwinNotificationCategoryText = 'textCategory'; + + /// Defines a iOS/MacOS notification category for plain actions. + static const String darwinNotificationCategoryPlain = 'plainCategory'; + + /// A notification action which triggers a url launch event + static const String urlLaunchActionId = 'id_1'; + + /// A notification action which triggers a App navigation event + static const String navigationActionId = 'id_3'; + + static String? selectedNotificationPayload; + + /// Streams are created so that app can respond to notification-related events + /// since the plugin is initialised in the `main` function + static final StreamController + didReceiveLocalNotificationStream = + StreamController.broadcast(); + + static final StreamController selectNotificationStream = + StreamController.broadcast(); + + static Future initNotification() async { + await _configureLocalTimeZone(); + final NotificationAppLaunchDetails? notificationAppLaunchDetails = + !kIsWeb && Platform.isLinux + ? null + : await flutterLocalNotificationsPlugin + .getNotificationAppLaunchDetails(); + // String initialRoute = HomePage.routeName; + if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) { + selectedNotificationPayload = + notificationAppLaunchDetails!.notificationResponse?.payload; + // initialRoute = SecondPage.routeName; + } + + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('app_icon'); + + final List darwinNotificationCategories = + [ + DarwinNotificationCategory( + darwinNotificationCategoryText, + actions: [ + DarwinNotificationAction.text( + 'view_scan', + 'View scan', + buttonTitle: 'View', + placeholder: 'Placeholder', + ), + ], + ), + ]; + + /// Note: permissions aren't requested here just to demonstrate that can be + /// done later + final DarwinInitializationSettings initializationSettingsDarwin = + DarwinInitializationSettings( + requestAlertPermission: false, + requestBadgePermission: false, + requestSoundPermission: false, + onDidReceiveLocalNotification: + (int id, String? title, String? body, String? payload) async { + didReceiveLocalNotificationStream.add( + ReceivedNotification( + id: id, + title: title, + body: body, + payload: payload, + ), + ); + }, + notificationCategories: darwinNotificationCategories, + ); + final LinuxInitializationSettings initializationSettingsLinux = + LinuxInitializationSettings( + defaultActionName: 'Open notification', + defaultIcon: AssetsLinuxIcon('app_icon.png'), + ); + final InitializationSettings initializationSettings = + InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsDarwin, + macOS: initializationSettingsDarwin, + linux: initializationSettingsLinux, + ); + await flutterLocalNotificationsPlugin.initialize( + initializationSettings, + onDidReceiveNotificationResponse: + (NotificationResponse notificationResponse) { + switch (notificationResponse.notificationResponseType) { + case NotificationResponseType.selectedNotification: + selectNotificationStream.add(notificationResponse.payload); + break; + case NotificationResponseType.selectedNotificationAction: + if (notificationResponse.actionId == navigationActionId) { + selectNotificationStream.add(notificationResponse.payload); + } + break; + } + }, + ); + } + + static Future _configureLocalTimeZone() async { + if (kIsWeb || Platform.isLinux) { + return; + } + tz.initializeTimeZones(); + final String timeZoneName = await FlutterTimezone.getLocalTimezone(); + tz.setLocalLocation(tz.getLocation(timeZoneName)); + } + + static Future showNotificationWithActions() async { + const AndroidNotificationDetails androidNotificationDetails = + AndroidNotificationDetails( + 'your channel id', + 'your channel name', + channelDescription: 'your channel description', + importance: Importance.max, + priority: Priority.high, + ticker: 'ticker', + actions: [ + AndroidNotificationAction( + urlLaunchActionId, + 'View', + ), + ], + ); + + const DarwinNotificationDetails iosNotificationDetails = + DarwinNotificationDetails( + categoryIdentifier: darwinNotificationCategoryPlain, + ); + + const DarwinNotificationDetails macOSNotificationDetails = + DarwinNotificationDetails( + categoryIdentifier: darwinNotificationCategoryPlain, + ); + + const LinuxNotificationDetails linuxNotificationDetails = + LinuxNotificationDetails( + actions: [ + LinuxNotificationAction( + key: urlLaunchActionId, + label: 'View', + ), + ], + ); + + const NotificationDetails notificationDetails = NotificationDetails( + android: androidNotificationDetails, + iOS: iosNotificationDetails, + macOS: macOSNotificationDetails, + linux: linuxNotificationDetails, + ); + await flutterLocalNotificationsPlugin.show( + id++, + 'Scan completed', + 'Your devices scan has been completed successfully', + notificationDetails, + payload: 'item z', + ); + } + + static Future grantPermissions() async { + await _isAndroidPermissionGranted(); + await _requestPermissions(); + } + + static Future _isAndroidPermissionGranted() async { + if (Platform.isAndroid) { + return await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.areNotificationsEnabled() ?? + false; + } + return false; + } + + static Future _requestPermissions() async { + if (Platform.isIOS || Platform.isMacOS) { + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + IOSFlutterLocalNotificationsPlugin>() + ?.requestPermissions( + alert: true, + badge: true, + sound: true, + ); + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + MacOSFlutterLocalNotificationsPlugin>() + ?.requestPermissions( + alert: true, + badge: true, + sound: true, + ); + } else if (Platform.isAndroid) { + final AndroidFlutterLocalNotificationsPlugin? androidImplementation = + flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>(); + + return await androidImplementation?.requestNotificationsPermission(); + } + return false; + } +} diff --git a/lib/services/impls/device_scanner_service.dart b/lib/services/impls/device_scanner_service.dart index 6d5a80c1..6d797e57 100644 --- a/lib/services/impls/device_scanner_service.dart +++ b/lib/services/impls/device_scanner_service.dart @@ -95,4 +95,12 @@ class DeviceScannerService extends ScannerService { } return const Stream.empty(); } + + Future getCurrentDevicesCount() async { + final scan = await _scanRepository.getOnGoingScan(); + if (scan != null) { + return _deviceRepository.countByScanId(scan.id); + } + return 0; + } } diff --git a/lib/values/strings.dart b/lib/values/strings.dart index bf16cf26..9066a843 100644 --- a/lib/values/strings.dart +++ b/lib/values/strings.dart @@ -19,4 +19,5 @@ class StringValue { static const String customSubnetDesc = 'Scan a custom subnet instead of local one.'; static const String customSubnetHint = 'e.g., 10.102.200.1'; + static const String hostScanPageTitle = 'Scan for devices'; } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index f44a2fc2..a23ca65c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import flutter_local_notifications +import flutter_timezone import in_app_review import isar_flutter_libs import network_info_plus @@ -15,6 +17,8 @@ import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) + FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 45b58840..8ba80377 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,4 +1,8 @@ PODS: + - flutter_local_notifications (0.0.1): + - FlutterMacOS + - flutter_timezone (0.1.0): + - FlutterMacOS - FlutterMacOS (1.0.0) - in_app_review (0.2.0): - FlutterMacOS @@ -20,6 +24,8 @@ PODS: - FlutterMacOS DEPENDENCIES: + - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) + - flutter_timezone (from `Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`) - isar_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos`) @@ -31,6 +37,10 @@ DEPENDENCIES: - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) EXTERNAL SOURCES: + flutter_local_notifications: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos + flutter_timezone: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos FlutterMacOS: :path: Flutter/ephemeral in_app_review: @@ -51,6 +61,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: + flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 + flutter_timezone: 6b906d1740654acb16e50b639835628fea851037 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 in_app_review: a850789fad746e89bce03d4aeee8078b45a53fd0 isar_flutter_libs: 43385c99864c168fadba7c9adeddc5d38838ca6a diff --git a/pubspec.yaml b/pubspec.yaml index bcb4dad8..54a25b1e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,8 +24,12 @@ dependencies: sdk: flutter # Bloc for state management, replace StatefulWidget flutter_bloc: ^8.1.1 + # A cross platform plugin for displaying local notifications. + flutter_local_notifications: ^17.2.2 # Native splash screen plugin flutter_native_splash: ^2.4.0 + # A flutter plugin for getting the local timezone of the OS. + flutter_timezone: ^3.0.0 # Annotations for freezed freezed_annotation: ^2.4.1 # Service locator @@ -46,7 +50,11 @@ dependencies: json_annotation: ^4.8.1 network_info_plus: ^4.0.2 # Helps you discover open ports, devices on subnet and more. - network_tools_flutter: ^2.0.0 + # network_tools_flutter: ^2.0.0 + network_tools_flutter: + git: + url: https://github.com/osociety/network_tools_flutter.git + ref: dev # branch name # Querying information about the application package, such as CFBundleVersion package_info_plus: ^4.1.0 path_provider: ^2.1.1 @@ -58,6 +66,8 @@ dependencies: provider: ^6.0.4 # Reading and writing simple key-value pairs shared_preferences: ^2.0.15 + # Time zone database and time zone aware DateTime. + timezone: ^0.9.4 # Plugin for launching a URL url_launcher: ^6.1.6 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8af0cec7..07885a4c 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterTimezonePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterTimezonePluginCApi")); IsarFlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); NsdWindowsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index e69cb1ae..974bb5ed 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_timezone isar_flutter_libs nsd_windows permission_handler_windows