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