diff --git a/.github/workflows/flutter_release.yml b/.github/workflows/flutter_release.yml index 5a42ade1..8f516d96 100644 --- a/.github/workflows/flutter_release.yml +++ b/.github/workflows/flutter_release.yml @@ -44,7 +44,7 @@ jobs: run: flutter pub upgrade - name: Run build_runner - run: flutter pub run build_runner build --delete-conflicting-outputs + run: dart run build_runner build --delete-conflicting-outputs - name: Download Android keystore id: android_keystore @@ -154,7 +154,7 @@ jobs: run: flutter pub upgrade - name: Run build_runner - run: flutter pub run build_runner build --delete-conflicting-outputs + run: dart run build_runner build --delete-conflicting-outputs - name: Build macos release run: flutter build macos --release @@ -208,7 +208,7 @@ jobs: run: flutter pub upgrade - name: Run build_runner - run: flutter pub run build_runner build --delete-conflicting-outputs + run: dart run build_runner build --delete-conflicting-outputs - name: Build windows release run: flutter build windows --release diff --git a/lib/api/isp_loader.dart b/lib/api/isp_loader.dart index a039bd0a..650c5db8 100644 --- a/lib/api/isp_loader.dart +++ b/lib/api/isp_loader.dart @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:vernet/models/internet_provider.dart'; +import 'package:vernet/providers/internet_provider.dart'; class ISPLoader { static Future loadIP(String url) async { diff --git a/lib/helper/app_settings.dart b/lib/helper/app_settings.dart index caa717e8..49e31a07 100644 --- a/lib/helper/app_settings.dart +++ b/lib/helper/app_settings.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; class AppSettings { + //TODO: move it to isar db AppSettings._(); static const String _lastSubnetKey = 'AppSettings-LAST_SUBNET'; @@ -9,12 +10,14 @@ class AppSettings { static const String _socketTimeoutKey = 'AppSettings-SOCKET_TIMEOUT'; static const String _pingCountKey = 'AppSettings-PING_COUNT'; static const String _inAppInternetKey = 'AppSettings-IN-APP-INTERNET'; + static const String _runScanOnStartupKey = 'AppSettings-RUN-SCAN-ON-STARTUP'; static const String _customSubnetKey = 'AppSettings-CUSTOM-SUBNET'; int _firstSubnet = 1; int _lastSubnet = 254; int _socketTimeout = 500; int _pingCount = 5; bool _inAppInternet = false; + bool _runScanOnStartup = false; String _customSubnet = ''; static final AppSettings _instance = AppSettings._(); @@ -25,6 +28,7 @@ class AppSettings { int get socketTimeout => _socketTimeout; int get pingCount => _pingCount; bool get inAppInternet => _inAppInternet; + bool get runScanOnStartup => _runScanOnStartup; String get customSubnet => _customSubnet; String get gatewayIP => _customSubnet.isNotEmpty ? _customSubnet.substring(0, _customSubnet.lastIndexOf('.')) @@ -60,6 +64,12 @@ class AppSettings { .setBool(_inAppInternetKey, _inAppInternet); } + Future setRunScanOnStartup(bool runScanOnStartup) async { + _runScanOnStartup = runScanOnStartup; + return (await SharedPreferences.getInstance()) + .setBool(_runScanOnStartupKey, _runScanOnStartup); + } + Future setCustomSubnet(String customSubnet) async { _customSubnet = customSubnet; return (await SharedPreferences.getInstance()) @@ -93,6 +103,11 @@ class AppSettings { _inAppInternet; debugPrint("In-App Internet : $_inAppInternet"); + _runScanOnStartup = + (await SharedPreferences.getInstance()).getBool(_runScanOnStartupKey) ?? + runScanOnStartup; + debugPrint("Run scan on startup : $_runScanOnStartup"); + _customSubnet = (await SharedPreferences.getInstance()).getString(_customSubnetKey) ?? _customSubnet; diff --git a/lib/helper/dark_theme_preference.dart b/lib/helper/dark_theme_preference.dart index 99f4dedc..56d813f5 100644 --- a/lib/helper/dark_theme_preference.dart +++ b/lib/helper/dark_theme_preference.dart @@ -1,5 +1,5 @@ import 'package:shared_preferences/shared_preferences.dart'; -import 'package:vernet/models/dark_theme_provider.dart'; +import 'package:vernet/providers/dark_theme_provider.dart'; class DarkThemePreference { static const themeStatus = 'THEMESTATUS_NEW'; diff --git a/lib/helper/utils_helper.dart b/lib/helper/utils_helper.dart index fac875cb..d4f307b2 100644 --- a/lib/helper/utils_helper.dart +++ b/lib/helper/utils_helper.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:vernet/ui/external_link_dialog.dart'; @@ -14,3 +15,11 @@ Future launchURLWithWarning(BuildContext context, String url) { ), ); } + +Future storeCurrentScanId(int scanId) async { + (await SharedPreferences.getInstance()).setInt('CurrentScanIDKey', scanId); +} + +Future getCurrentScanId() async { + return (await SharedPreferences.getInstance()).getInt('CurrentScanIDKey'); +} diff --git a/lib/main.dart b/lib/main.dart index b3828536..f7e72c56 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ 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'; @@ -7,10 +8,11 @@ import 'package:vernet/api/update_checker.dart'; import 'package:vernet/helper/app_settings.dart'; import 'package:vernet/helper/consent_loader.dart'; import 'package:vernet/injection.dart'; -import 'package:vernet/models/dark_theme_provider.dart'; import 'package:vernet/pages/home_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'; AppSettings appSettings = AppSettings.instance; Future main() async { @@ -45,6 +47,20 @@ class _MyAppState extends State { void initState() { super.initState(); 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 { diff --git a/lib/pages/host_scan_page/device_in_the_network.dart b/lib/models/device_in_the_network.dart similarity index 94% rename from lib/pages/host_scan_page/device_in_the_network.dart rename to lib/models/device_in_the_network.dart index 7e9c61c5..ad5765fc 100644 --- a/lib/pages/host_scan_page/device_in_the_network.dart +++ b/lib/models/device_in_the_network.dart @@ -12,6 +12,8 @@ class DeviceInTheNetwork { required this.internetAddress, required Future makeVar, required this.pingData, + required this.currentDeviceIp, + required this.gatewayIp, MdnsInfo? mdnsVar, String? mac, this.iconData = Icons.devices, @@ -71,6 +73,8 @@ class DeviceInTheNetwork { internetAddress: internetAddress, makeVar: deviceMake, pingData: pingData, + currentDeviceIp: currentDeviceIp, + gatewayIp: gatewayIp, hostId: hostId, iconData: iconData, mdnsVar: mdns, @@ -80,6 +84,8 @@ class DeviceInTheNetwork { /// Ip of the device final InternetAddress internetAddress; + final String currentDeviceIp; + final String gatewayIp; late Future make; String? _mac; @@ -96,14 +102,13 @@ class DeviceInTheNetwork { set mdns(MdnsInfo? name) { _mdns = name; - final Future deviceMake = getDeviceMake( + make = getDeviceMake( currentDeviceIp: '', hostIp: internetAddress.address, gatewayIp: '', hostMake: make, mdns: _mdns, ); - make = deviceMake; } /// Some name to show the user diff --git a/lib/models/isar/device.dart b/lib/models/isar/device.dart new file mode 100644 index 00000000..95518288 --- /dev/null +++ b/lib/models/isar/device.dart @@ -0,0 +1,64 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:isar/isar.dart'; + +part 'device.g.dart'; + +@collection +class Device { + Device({ + required this.internetAddress, + required this.hostMake, + required this.currentDeviceIp, + required this.gatewayIp, + required this.scanId, + this.mdnsDomainName, + this.macAddress, + }); + final Id id = Isar.autoIncrement; + @Index(type: IndexType.value) + final int scanId; + @Index(type: IndexType.value) + final String internetAddress; + final String currentDeviceIp; + final String gatewayIp; + final String? macAddress; + final String? hostMake; + final String? mdnsDomainName; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Device && + runtimeType == other.runtimeType && + internetAddress == other.internetAddress; + + @override + int get hashCode => internetAddress.hashCode; + + @ignore + String? get deviceMake { + if (currentDeviceIp == internetAddress) { + return 'This device'; + } else if (gatewayIp == internetAddress) { + return 'Router/Gateway'; + } else if (mdnsDomainName != null) { + return mdnsDomainName; + } + return hostMake; + } + + @ignore + IconData get iconData { + if (internetAddress == currentDeviceIp) { + if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { + return Icons.computer; + } + return Icons.smartphone; + } else if (internetAddress == gatewayIp) { + return Icons.router; + } + return Icons.devices; + } +} diff --git a/lib/models/isar/scan.dart b/lib/models/isar/scan.dart new file mode 100644 index 00000000..176b2fba --- /dev/null +++ b/lib/models/isar/scan.dart @@ -0,0 +1,18 @@ +import 'package:isar/isar.dart'; +part 'scan.g.dart'; + +@collection +class Scan { + Scan({ + required this.gatewayIp, + this.startTime, + this.endTime, + this.onGoing, + }); + Id id = Isar.autoIncrement; + @Index(type: IndexType.value) + final String gatewayIp; + bool? onGoing; + DateTime? startTime; + DateTime? endTime; +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index e7f83f60..492e076b 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -6,13 +6,13 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:vernet/api/isp_loader.dart'; import 'package:vernet/helper/utils_helper.dart'; import 'package:vernet/main.dart'; -import 'package:vernet/models/internet_provider.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'; 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/ui/adaptive/adaptive_list.dart'; import 'package:vernet/ui/custom_tile.dart'; 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 fa061228..dc48135f 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 @@ -6,8 +6,14 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:injectable/injectable.dart'; import 'package:network_info_plus/network_info_plus.dart'; import 'package:network_tools_flutter/network_tools_flutter.dart'; +import 'package:vernet/helper/utils_helper.dart'; +import 'package:vernet/injection.dart'; import 'package:vernet/main.dart'; -import 'package:vernet/pages/host_scan_page/device_in_the_network.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/scan_repository.dart'; +import 'package:vernet/services/impls/device_scanner_service.dart'; part 'host_scan_bloc.freezed.dart'; part 'host_scan_event.dart'; @@ -18,7 +24,9 @@ class HostScanBloc extends Bloc { HostScanBloc() : super(HostScanState.initial()) { on(_initialized); on(_startNewScanBuiltInIsolate); + on(_loadScanAndShowResults); } + final scannerService = getIt(); /// IP of the device in the local network. String? ip; @@ -46,101 +54,54 @@ class HostScanBloc extends Bloc { ? appSettings.customSubnet : await NetworkInfo().getWifiGatewayIP(); subnet = gatewayIp!.substring(0, gatewayIp!.lastIndexOf('.')); - add(const HostScanEvent.startNewScan()); + if (appSettings.runScanOnStartup) { + add(const HostScanEvent.loadScan()); + } else { + add(const HostScanEvent.startNewScan()); + } } Future _startNewScanBuiltInIsolate( StartNewScan event, Emitter emit, ) async { - final streamController = HostScannerService.instance.getAllPingableDevices( - subnet!, - firstHostId: appSettings.firstSubnet, - lastHostId: appSettings.lastSubnet, - ); - await for (final ActiveHost activeHost in streamController) { - final int index = indexOfActiveHost(activeHost.address); - - if (index == -1) { - deviceInTheNetworkList.add( - DeviceInTheNetwork.createFromActiveHost( - activeHost: activeHost, - currentDeviceIp: ip!, - gatewayIp: gatewayIp!, - mac: (await activeHost.arpData)?.macAddress, - ), - ); - } else { - deviceInTheNetworkList[index] = DeviceInTheNetwork.createFromActiveHost( - activeHost: activeHost, - currentDeviceIp: ip!, - gatewayIp: gatewayIp!, - mdns: deviceInTheNetworkList[index].mdns, - mac: (await activeHost.arpData)?.macAddress, - ); - } + emit(const HostScanState.loadInProgress()); - deviceInTheNetworkList.sort(sort); - emit(const HostScanState.loadInProgress()); - emit(HostScanState.foundNewDevice(deviceInTheNetworkList)); + final Set devices = {}; + final deviceStream = + getIt().startNewScan(subnet!, ip!, gatewayIp!); + await for (final Device device in deviceStream) { + devices.add(device); + emit(HostScanState.foundNewDevice(devices)); } - final activeMdnsHostList = - await MdnsScannerService.instance.searchMdnsDevices(); - - for (final ActiveHost activeHost in activeMdnsHostList) { - final int index = indexOfActiveHost(activeHost.address); - final MdnsInfo? mDns = await activeHost.mdnsInfo; - if (mDns == null) { - continue; - } - - if (index == -1) { - deviceInTheNetworkList.add( - DeviceInTheNetwork.createFromActiveHost( - activeHost: activeHost, - currentDeviceIp: ip!, - gatewayIp: gatewayIp!, - mdns: mDns, - mac: (await activeHost.arpData)?.macAddress, - ), - ); - } else { - deviceInTheNetworkList[index] = deviceInTheNetworkList[index] - ..mdns = mDns; - } - - deviceInTheNetworkList.sort(sort); - emit(const HostScanState.loadInProgress()); - emit(HostScanState.foundNewDevice(deviceInTheNetworkList)); - } - emit(HostScanState.loadSuccess(deviceInTheNetworkList)); + emit(HostScanState.loadSuccess(devices)); } - /// Getting active host IP and finds it's index inside of activeHostList - /// Returns -1 if didn't find - int indexOfActiveHost(String ip) { - return deviceInTheNetworkList - .indexWhere((element) => element.internetAddress.address == ip); - } + Future _loadScanAndShowResults( + LoadScan event, + Emitter emit, + ) async { + emit(const HostScanState.loadInProgress()); - int sort(DeviceInTheNetwork a, DeviceInTheNetwork b) { - final regexA = a.internetAddress.address.contains('.') ? '.' : '::'; - final regexB = b.internetAddress.address.contains('.') ? '.' : '::'; - if (regexA.length == 2 || regexB.length == 2) { - return regexA.length.compareTo(regexB.length); + final Set devicesSet = {}; + final deviceStream = await getIt().getOnGoingScan(); + deviceStream.listen((devices) { + devicesSet.addAll(devices); + emit(HostScanState.foundNewDevice(devicesSet)); + }); + + //load success based on scan record getting updated to ongoing = true + final currentScanId = await getCurrentScanId(); + if (currentScanId != null) { + final scanStream = await getIt().watch(currentScanId); + await for (final List scanList in scanStream) { + final scan = scanList.first; + if (scan.onGoing == false) { + emit(HostScanState.loadSuccess(devicesSet)); + break; + } + } } - final int aIp = int.parse( - a.internetAddress.address.substring( - a.internetAddress.address.lastIndexOf(regexA) + regexA.length, - ), - ); - final int bIp = int.parse( - b.internetAddress.address.substring( - b.internetAddress.address.lastIndexOf(regexB) + regexB.length, - ), - ); - - return aIp.compareTo(bIp); } } diff --git a/lib/pages/host_scan_page/host_scan_bloc/host_scan_event.dart b/lib/pages/host_scan_page/host_scan_bloc/host_scan_event.dart index 88e500f7..93548888 100644 --- a/lib/pages/host_scan_page/host_scan_bloc/host_scan_event.dart +++ b/lib/pages/host_scan_page/host_scan_bloc/host_scan_event.dart @@ -5,4 +5,5 @@ class HostScanEvent with _$HostScanEvent { const factory HostScanEvent.initialized() = Initialized; const factory HostScanEvent.startNewScan() = StartNewScan; + const factory HostScanEvent.loadScan() = LoadScan; } diff --git a/lib/pages/host_scan_page/host_scan_bloc/host_scan_state.dart b/lib/pages/host_scan_page/host_scan_bloc/host_scan_state.dart index 4b35f502..e1400c68 100644 --- a/lib/pages/host_scan_page/host_scan_bloc/host_scan_state.dart +++ b/lib/pages/host_scan_page/host_scan_bloc/host_scan_state.dart @@ -7,11 +7,11 @@ class HostScanState with _$HostScanState { const factory HostScanState.loadInProgress() = _LoadInProgress; const factory HostScanState.foundNewDevice( - List activeHostList, + Set activeHosts, ) = FoundNewDevice; const factory HostScanState.loadSuccess( - List activeHostList, + Set activeHosts, ) = LoadSuccess; const factory HostScanState.loadFailure() = _loadFailure; diff --git a/lib/pages/host_scan_page/widgets/host_scan_widget.dart b/lib/pages/host_scan_page/widgets/host_scan_widget.dart index 11eb93f4..cac3507c 100644 --- a/lib/pages/host_scan_page/widgets/host_scan_widget.dart +++ b/lib/pages/host_scan_page/widgets/host_scan_widget.dart @@ -2,11 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:vernet/main.dart'; -import 'package:vernet/pages/host_scan_page/device_in_the_network.dart'; +import 'package:vernet/models/isar/device.dart'; import 'package:vernet/pages/host_scan_page/host_scan_bloc/host_scan_bloc.dart'; import 'package:vernet/pages/network_troubleshoot/port_scan_page.dart'; import 'package:vernet/ui/adaptive/adaptive_list.dart'; +//TODO: Device doesn't refresh when active scan going on class HostScanWidget extends StatelessWidget { @override Widget build(BuildContext context) { @@ -37,13 +38,13 @@ class HostScanWidget extends StatelessWidget { ); }, foundNewDevice: (FoundNewDevice value) { - return _devicesWidget(context, value.activeHostList, true); + return _devicesWidget(context, value.activeHosts.toList(), true); }, loadFailure: (value) { return const Text('Failure'); }, loadSuccess: (value) { - return _devicesWidget(context, value.activeHostList, false); + return _devicesWidget(context, value.activeHosts.toList(), false); }, error: (Error value) { return const Text('Error'); @@ -55,7 +56,7 @@ class HostScanWidget extends StatelessWidget { Widget _devicesWidget( BuildContext context, - List activeHostList, + List activeHostList, bool loading, ) { return Flex( @@ -79,7 +80,7 @@ class HostScanWidget extends StatelessWidget { onPressed: () { context .read() - .add(const HostScanEvent.initialized()); + .add(const HostScanEvent.startNewScan()); }, icon: const Icon(Icons.replay), ), @@ -88,18 +89,12 @@ class HostScanWidget extends StatelessWidget { child: ListView.builder( itemCount: activeHostList.length, itemBuilder: (context, index) { - final DeviceInTheNetwork host = activeHostList[index]; + final Device host = activeHostList[index]; return AdaptiveListTile( leading: Icon(host.iconData), - title: FutureBuilder( - future: host.make, - builder: (context, AsyncSnapshot snapshot) { - return Text(snapshot.data ?? ''); - }, - initialData: 'Generic Device', - ), + title: Text(host.deviceMake ?? ''), subtitle: Text( - '${host.internetAddress.address} ${host.mac}', + '${host.internetAddress} (${host.macAddress ?? ''})', ), trailing: IconButton( tooltip: 'Scan open ports for this target', @@ -109,7 +104,7 @@ class HostScanWidget extends StatelessWidget { context, MaterialPageRoute( builder: (context) => PortScanPage( - target: host.internetAddress.address, + target: host.internetAddress, ), ), ); @@ -118,7 +113,7 @@ class HostScanWidget extends StatelessWidget { onLongPress: () { Clipboard.setData( ClipboardData( - text: host.internetAddress.address, + text: host.internetAddress, ), ); ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 95a4b635..739f500f 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:in_app_review/in_app_review.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:provider/provider.dart'; import 'package:vernet/api/update_checker.dart'; import 'package:vernet/helper/utils_helper.dart'; import 'package:vernet/main.dart'; -import 'package:vernet/models/dark_theme_provider.dart'; +import 'package:vernet/providers/dark_theme_provider.dart'; import 'package:vernet/ui/adaptive/adaptive_list.dart'; import 'package:vernet/ui/settings_dialog/custom_subnet_dialog.dart'; import 'package:vernet/ui/settings_dialog/first_subnet_dialog.dart'; @@ -22,6 +23,8 @@ class SettingsPage extends StatefulWidget { } class _SettingsPageState extends State { + final InAppReview inAppReview = InAppReview.instance; + @override Widget build(BuildContext context) { final themeChange = Provider.of(context); @@ -55,6 +58,19 @@ class _SettingsPageState extends State { ), ), ), + Card( + child: AdaptiveListTile( + title: const Text('Run scan on app startup'), + trailing: Switch( + value: appSettings.runScanOnStartup, + onChanged: (bool? value) async { + appSettings.setRunScanOnStartup(value ?? false); + await appSettings.load(); + setState(() {}); + }, + ), + ), + ), Card( child: AdaptiveListTile( title: const Text(StringValue.firstSubnet), @@ -171,6 +187,14 @@ class _SettingsPageState extends State { ), ), ), + Card( + child: AdaptiveListTile( + title: const Text('Rate our app'), + onTap: () { + inAppReview.openStoreListing(); + }, + ), + ), Card( child: AdaptiveListTile( title: const Text('About'), diff --git a/lib/models/dark_theme_provider.dart b/lib/providers/dark_theme_provider.dart similarity index 100% rename from lib/models/dark_theme_provider.dart rename to lib/providers/dark_theme_provider.dart diff --git a/lib/models/internet_provider.dart b/lib/providers/internet_provider.dart similarity index 100% rename from lib/models/internet_provider.dart rename to lib/providers/internet_provider.dart diff --git a/lib/repository/device_repository.dart b/lib/repository/device_repository.dart new file mode 100644 index 00000000..1b9d08da --- /dev/null +++ b/lib/repository/device_repository.dart @@ -0,0 +1,52 @@ +import 'package:injectable/injectable.dart'; +import 'package:isar/isar.dart'; +import 'package:vernet/models/isar/device.dart'; +import 'package:vernet/repository/repository.dart'; +import 'package:vernet/services/database_service.dart'; + +@Injectable() +class DeviceRepository extends IsarRepository { + DeviceRepository(this._database); + final DatabaseService _database; + + @override + Future get(Id id) async { + final deviceDB = await _database.open(); + return deviceDB!.devices.get(id); + } + + @override + Future> getList() async { + final deviceDB = await _database.open(); + return deviceDB!.devices.where().findAll(); + } + + @override + Future put(Device device) async { + final deviceDB = await _database.open(); + await deviceDB!.writeTxn(() async { + await deviceDB.devices.put(device); + }); + return device; + } + + Future getDevice(int scanId, String address) async { + final deviceDB = await _database.open(); + return deviceDB!.devices + .filter() + .scanIdEqualTo(scanId) + .and() + .internetAddressEqualTo(address) + .findFirst(); + } + + Future>> watch(int scanId) async { + final deviceDB = await _database.open(); + return deviceDB!.devices + .filter() + .scanIdEqualTo(scanId) + .sortByInternetAddress() + .build() + .watch(fireImmediately: true); + } +} diff --git a/lib/repository/repository.dart b/lib/repository/repository.dart new file mode 100644 index 00000000..0b1ce97b --- /dev/null +++ b/lib/repository/repository.dart @@ -0,0 +1,7 @@ +import 'package:isar/isar.dart'; + +abstract class IsarRepository { + Future> getList(); + Future get(Id id); + Future put(T t); +} diff --git a/lib/repository/scan_repository.dart b/lib/repository/scan_repository.dart new file mode 100644 index 00000000..cb8fb78d --- /dev/null +++ b/lib/repository/scan_repository.dart @@ -0,0 +1,56 @@ +import 'package:injectable/injectable.dart'; +import 'package:isar/isar.dart'; +import 'package:vernet/helper/utils_helper.dart'; +import 'package:vernet/models/isar/scan.dart'; +import 'package:vernet/repository/repository.dart'; +import 'package:vernet/services/database_service.dart'; + +@Injectable() +class ScanRepository extends IsarRepository { + ScanRepository(this._database); + final DatabaseService _database; + + @override + Future> getList() async { + final scanDB = await _database.open(); + return scanDB!.scans.where().findAll(); + } + + @override + Future get(Id id) async { + final scanDB = await _database.open(); + return scanDB!.scans.get(id); + } + + @override + Future put(Scan scan) async { + final scanDB = await _database.open(); + await scanDB!.writeTxn(() async { + await scanDB.scans.put(scan); + }); + return scan; + } + + Future getOnGoingScan() async { + final scanDB = await _database.open(); + final ongoingScanId = await getCurrentScanId(); + if (ongoingScanId != null) { + return get(ongoingScanId); + } + return scanDB!.scans + .filter() + .onGoingEqualTo(true) + .endTimeEqualTo(null) + .sortByStartTimeDesc() + .findFirst(); + } + + Future>> watch(int id) async { + final scanDB = await _database.open(); + return scanDB!.scans + .filter() + .idEqualTo(id) + .build() + .watch(fireImmediately: true); + } +} diff --git a/lib/services/database_service.dart b/lib/services/database_service.dart new file mode 100644 index 00000000..bd13669b --- /dev/null +++ b/lib/services/database_service.dart @@ -0,0 +1,5 @@ +import 'package:isar/isar.dart'; + +abstract class DatabaseService { + Future open(); +} diff --git a/lib/services/impls/device_scanner_service.dart b/lib/services/impls/device_scanner_service.dart new file mode 100644 index 00000000..ca3b7878 --- /dev/null +++ b/lib/services/impls/device_scanner_service.dart @@ -0,0 +1,98 @@ +import 'package:flutter/foundation.dart'; +import 'package:injectable/injectable.dart'; +import 'package:network_tools_flutter/network_tools_flutter.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/isar/scan.dart'; +import 'package:vernet/repository/device_repository.dart'; +import 'package:vernet/repository/scan_repository.dart'; +import 'package:vernet/services/scanner_service.dart'; + +@Injectable() +class DeviceScannerService extends ScannerService { + static final _scanRepository = getIt(); + static final _deviceRepository = getIt(); + + @override + Stream startNewScan( + String subnet, + String ip, + String gatewayIp, + ) async* { + final scan = await _scanRepository.put( + Scan( + gatewayIp: subnet, + startTime: DateTime.now(), + onGoing: true, + ), + ); + + await storeCurrentScanId(scan.id); + + final streamController = HostScannerService.instance.getAllPingableDevices( + subnet, + firstHostId: appSettings.firstSubnet, + lastHostId: appSettings.lastSubnet, + ); + await for (final ActiveHost activeHost in streamController) { + var device = + await _deviceRepository.getDevice(scan.id, activeHost.address); + if (device == null) { + device = Device( + internetAddress: activeHost.address, + macAddress: (await activeHost.arpData)!.macAddress, + currentDeviceIp: ip, + hostMake: await activeHost.deviceName, + gatewayIp: gatewayIp, + scanId: scan.id, + ); + await _deviceRepository.put(device); + } + debugPrint('Device found: ${device.internetAddress}'); + yield device; + } + + final activeMdnsHostList = + await MdnsScannerService.instance.searchMdnsDevices(); + + for (final ActiveHost activeHost in activeMdnsHostList) { + var device = + await _deviceRepository.getDevice(scan.id, activeHost.address); + + final MdnsInfo? mDns = await activeHost.mdnsInfo; + if (mDns == null) { + continue; + } + + if (device == null) { + device = Device( + internetAddress: activeHost.address, + macAddress: (await activeHost.arpData)?.macAddress, + hostMake: await activeHost.deviceName, + currentDeviceIp: ip, + gatewayIp: gatewayIp, + scanId: scan.id, + ); + await _deviceRepository.put(device); + } + debugPrint('Device found: ${device.internetAddress}'); + yield device; + } + + scan.endTime = DateTime.now(); + scan.onGoing = false; + await _scanRepository.put(scan); + debugPrint('Scan ended'); + } + + @override + Future>> getOnGoingScan() async { + final scan = await _scanRepository.getOnGoingScan(); + if (scan != null) { + return _deviceRepository.watch(scan.id); + } + return const Stream.empty(); + } +} diff --git a/lib/services/impls/isar_database_service.dart b/lib/services/impls/isar_database_service.dart new file mode 100644 index 00000000..cc09ce26 --- /dev/null +++ b/lib/services/impls/isar_database_service.dart @@ -0,0 +1,18 @@ +import 'package:injectable/injectable.dart'; +import 'package:isar/isar.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:vernet/models/isar/device.dart'; +import 'package:vernet/models/isar/scan.dart'; +import 'package:vernet/services/database_service.dart'; + +@Injectable(as: DatabaseService) +class IsarDatabaseService extends DatabaseService { + static Isar? isarDb; + @override + Future open() async { + return isarDb ??= await Isar.open( + [ScanSchema, DeviceSchema], + directory: (await getApplicationDocumentsDirectory()).path, + ); + } +} diff --git a/lib/services/scanner_service.dart b/lib/services/scanner_service.dart new file mode 100644 index 00000000..73ed639b --- /dev/null +++ b/lib/services/scanner_service.dart @@ -0,0 +1,11 @@ +import 'package:vernet/models/isar/device.dart'; + +abstract class ScannerService { + Stream startNewScan( + String subnet, + String ip, + String gatewayIp, + ); + + Future>> getOnGoingScan(); +} diff --git a/lib/ui/adaptive/adaptive_dialog.dart b/lib/ui/adaptive/adaptive_dialog.dart index 118ced84..2629b3fa 100644 --- a/lib/ui/adaptive/adaptive_dialog.dart +++ b/lib/ui/adaptive/adaptive_dialog.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:vernet/models/dark_theme_provider.dart'; +import 'package:vernet/providers/dark_theme_provider.dart'; class AdaptiveDialog extends StatelessWidget { const AdaptiveDialog({ diff --git a/lib/ui/adaptive/adaptive_list.dart b/lib/ui/adaptive/adaptive_list.dart index 41e9cf46..d4c9b2e6 100644 --- a/lib/ui/adaptive/adaptive_list.dart +++ b/lib/ui/adaptive/adaptive_list.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:vernet/models/dark_theme_provider.dart'; +import 'package:vernet/providers/dark_theme_provider.dart'; class AdaptiveListTile extends StatelessWidget { const AdaptiveListTile({ diff --git a/lib/ui/settings_dialog/theme_dialog.dart b/lib/ui/settings_dialog/theme_dialog.dart index 4573ebd6..b063d6f7 100644 --- a/lib/ui/settings_dialog/theme_dialog.dart +++ b/lib/ui/settings_dialog/theme_dialog.dart @@ -1,7 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:vernet/models/dark_theme_provider.dart'; +import 'package:vernet/providers/dark_theme_provider.dart'; import 'package:vernet/ui/adaptive/adaptive_dialog.dart'; import 'package:vernet/ui/adaptive/adaptive_list.dart'; import 'package:vernet/ui/adaptive/adaptive_radio.dart'; diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index f6f23bfe..bfc0d083 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,9 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin"); + isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index f16b4c34..6237f02c 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + isar_flutter_libs url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 433026cd..f44a2fc2 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import in_app_review +import isar_flutter_libs import network_info_plus import nsd_macos import package_info_plus @@ -13,6 +15,8 @@ import shared_preferences_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) + IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin")) NsdMacosPlugin.register(with: registry.registrar(forPlugin: "NsdMacosPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index de7eb13e..45b58840 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,5 +1,9 @@ PODS: - FlutterMacOS (1.0.0) + - in_app_review (0.2.0): + - FlutterMacOS + - isar_flutter_libs (1.0.0): + - FlutterMacOS - network_info_plus (0.0.1): - FlutterMacOS - nsd_macos (0.0.1): @@ -17,6 +21,8 @@ PODS: DEPENDENCIES: - 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`) - network_info_plus (from `Flutter/ephemeral/.symlinks/plugins/network_info_plus/macos`) - nsd_macos (from `Flutter/ephemeral/.symlinks/plugins/nsd_macos/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) @@ -27,6 +33,10 @@ DEPENDENCIES: EXTERNAL SOURCES: FlutterMacOS: :path: Flutter/ephemeral + in_app_review: + :path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos + isar_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos network_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/network_info_plus/macos nsd_macos: @@ -42,12 +52,14 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + in_app_review: a850789fad746e89bce03d4aeee8078b45a53fd0 + isar_flutter_libs: 43385c99864c168fadba7c9adeddc5d38838ca6a network_info_plus: f4fbc7877ab7b3294500d9441dfa53cd54972d05 nsd_macos: 1a38a38a33adbb396b4c6f303bc076073514cadc package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c - shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 - url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 PODFILE CHECKSUM: 4d1ddd58dcd1dc92dd2b397bbacb622f345603ab diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index d53ef643..8e02df28 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/pubspec.yaml b/pubspec.yaml index 83c2dc59..f344cbab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,8 @@ version: 1.0.7+25 environment: sdk: ">=2.17.0 <3.0.0" +isar_version: &isar_version 3.1.0+1 + dependencies: # Automatically resizes text to fit perfectly within its bounds. auto_size_text: ^3.0.0 @@ -30,8 +32,14 @@ dependencies: get_it: ^7.2.0 # A composable, multi-platform, Future-based API for HTTP requests. http: ^1.1.0 + # A Flutter plugin that lets you show a review pop up where users can leave a review + in_app_review: ^2.0.9 # Convenient code generator for get_it injectable: ^2.1.0 + # database for flutter + isar: *isar_version + # contains Isar Core + isar_flutter_libs: *isar_version # An easy way to create a new isolate, keep it running and communicate with it. isolate_contactor: ^2.0.0+1 # Discover network info and configure themselves accordingly @@ -55,7 +63,7 @@ dependencies: dev_dependencies: # A build system for Dart code generation and modular compilation. - build_runner: ^2.4.9 + build_runner: any flutter_test: sdk: flutter @@ -63,6 +71,7 @@ dev_dependencies: freezed: ^2.5.0 # Convenient code generator for get_it. injectable_generator: ^2.1.4 + isar_generator: *isar_version # Collection of lint rules for Dart and Flutter projects. json_serializable: ^6.7.1 lint: ^2.0.1 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index b59afe29..8af0cec7 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,11 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + IsarFlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); NsdWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("NsdWindowsPluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index ce95a89f..e69cb1ae 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + isar_flutter_libs nsd_windows permission_handler_windows url_launcher_windows