diff --git a/.github/workflows/flutter_release.yml b/.github/workflows/flutter_release.yml index 34707e41..0d56c30b 100644 --- a/.github/workflows/flutter_release.yml +++ b/.github/workflows/flutter_release.yml @@ -16,9 +16,9 @@ jobs: runs-on: ubuntu-latest env: LINUX_ZIP: Vernet-${{github.ref_name}}-linux.zip - ANDROID_APK_ARM_V7A: app-armeabi-v7a-dev-debug.apk - ANDROID_APK_ARM_V8A: app-arm64-v8a-dev-debug.apk - ANDROID_APK_x86_64: app-x86_64-dev-debug.apk + ANDROID_APK_ARM_V7A: app-armeabi-v7a-dev-release.apk + ANDROID_APK_ARM_V8A: app-arm64-v8a-dev-release.apk + ANDROID_APK_x86_64: app-x86_64-dev-release.apk steps: - name: Checkout @@ -50,15 +50,11 @@ jobs: encodedString: '${{ secrets.ANDROID_KEYSTORE_BASE64 }}' - name: Create key.properties - run: > - echo "storeFile=${{ steps.android_keystore.outputs.filePath }}" > - android/key.properties - echo "storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" >> - android/key.properties - echo "keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}" >> - android/key.properties - echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" >> - android/key.properties + run: | + echo "storeFile=${{ steps.android_keystore.outputs.filePath }}" >> android/key.properties + echo "storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" >> android/key.properties + echo "keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}" >> android/key.properties + echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" >> android/key.properties - name: Create artifacts directory run: mkdir -p artifacts @@ -69,7 +65,7 @@ jobs: - name: Build Android App and Linux Bundle # Use signing keys for release instead of debug run: | - flutter build apk --debug --split-per-abi --flavor dev + flutter build apk --split-per-abi --flavor dev flutter build linux --release - name: Rename ANDROID APKs diff --git a/README.md b/README.md index 32bdc5fd..9aaf43f6 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,10 @@ Note: macOS build hasn't been notarized yet. ### Instructions for Linux 1. Star this repository. -2. Download vernet-linux.zip from [releases](https://github.com/git-elliot/vernet/releases/latest) -3. Extract downloaded zip file. -4. Go to bundle folder and double click vernet file. +2. Install `net-tools` package for `arp` command, otherwise app will not run. +3. Download vernet-linux.zip from [releases](https://github.com/git-elliot/vernet/releases/latest) +4. Extract downloaded zip file. +5. Go to bundle folder and double click vernet file. ## Contributors Required diff --git a/android/app/build.gradle b/android/app/build.gradle index 6f8019ea..1e42c596 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -31,7 +31,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 33 + compileSdkVersion 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -41,7 +41,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "org.fsociety.vernet" minSdkVersion 19 - targetSdkVersion 33 + targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } @@ -67,7 +67,7 @@ android { productFlavors { dev { dimension "deploy" - signingConfig signingConfigs.debug + signingConfig signingConfigs.release } fdroid { dimension "deploy" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ea67ebbf..24bf09b8 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ + + + + diff --git a/fastlane/metadata/android/en-US/changelogs/18.txt b/fastlane/metadata/android/en-US/changelogs/18.txt index 4b0202d7..db77bb61 100644 --- a/fastlane/metadata/android/en-US/changelogs/18.txt +++ b/fastlane/metadata/android/en-US/changelogs/18.txt @@ -1 +1 @@ -Follow system theme added and bug fixes \ No newline at end of file +Follow system theme added and bug fixes diff --git a/fastlane/metadata/android/en-US/changelogs/19.txt b/fastlane/metadata/android/en-US/changelogs/19.txt new file mode 100644 index 00000000..bcc900b1 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/19.txt @@ -0,0 +1 @@ +Many improvements and bug fixes \ No newline at end of file diff --git a/lib/api/update_checker.dart b/lib/api/update_checker.dart index ce4d4fa5..b963a2e2 100644 --- a/lib/api/update_checker.dart +++ b/lib/api/update_checker.dart @@ -1,12 +1,15 @@ import 'dart:convert'; import 'dart:io'; +import 'package:external_app_launcher/external_app_launcher.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:package_info_plus/package_info_plus.dart'; import 'package:vernet/helper/utils_helper.dart'; +import 'package:vernet/main.dart'; + Future _checkUpdates(String v) async { final Uri url = Uri.parse( 'https://api.github.com/repos/git-elliot/vernet/tags?per_page=1', @@ -37,7 +40,10 @@ Future checkForUpdates( try { final info = await PackageInfo.fromPlatform(); final String v = '${info.version}+${info.buildNumber}'; - final bool available = await compute(_checkUpdates, v); + bool available = false; + if (appSettings.inAppInternet) { + available = await compute(_checkUpdates, v); + } ScaffoldMessenger.of(context).clearSnackBars(); Widget? content; SnackBarAction? action; @@ -46,12 +52,16 @@ Future checkForUpdates( action = SnackBarAction( label: 'Update', onPressed: () { - _navigateToStore(); + _navigateToStore(context); }, ); } else { if (showIfNoUpdate) { content = const Text('No updates found'); + if (!appSettings.inAppInternet) { + content = + const Text('Please turn on In-App Internet to check updates.'); + } } } if (ScaffoldMessenger.of(context).mounted && content != null) { @@ -67,14 +77,28 @@ Future checkForUpdates( } } -Future _navigateToStore() async { +Future _navigateToStore(BuildContext context) async { String url = 'https://github.com/git-elliot/vernet/releases/latest'; + if (Platform.isAndroid) { + final isFdroidInstalled = await LaunchApp.isAppInstalled( + androidPackageName: 'org.fdroid.fdroid', + iosUrlScheme: 'fdroid://', + ); + if ((await PackageInfo.fromPlatform()).version.contains('store')) { //Goto playstore url = 'https://play.google.com/store/apps/details?id=org.fsociety.vernet.store'; + } else if (isFdroidInstalled == true) { + await LaunchApp.openApp( + androidPackageName: 'org.fdroid.fdroid', + iosUrlScheme: 'fdroid://', + appStoreLink: 'itms-apps://itunes.apple.com/', + openStore: false, + ); + return; } } - launchURL(url); + launchURLWithWarning(context, url); } diff --git a/lib/helper/app_settings.dart b/lib/helper/app_settings.dart index eb364a0a..caa717e8 100644 --- a/lib/helper/app_settings.dart +++ b/lib/helper/app_settings.dart @@ -8,10 +8,14 @@ class AppSettings { static const String _firstSubnetKey = 'AppSettings-FIRST_SUBNET'; 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 _customSubnetKey = 'AppSettings-CUSTOM-SUBNET'; int _firstSubnet = 1; int _lastSubnet = 254; int _socketTimeout = 500; int _pingCount = 5; + bool _inAppInternet = false; + String _customSubnet = ''; static final AppSettings _instance = AppSettings._(); @@ -20,6 +24,11 @@ class AppSettings { int get lastSubnet => _lastSubnet; int get socketTimeout => _socketTimeout; int get pingCount => _pingCount; + bool get inAppInternet => _inAppInternet; + String get customSubnet => _customSubnet; + String get gatewayIP => _customSubnet.isNotEmpty + ? _customSubnet.substring(0, _customSubnet.lastIndexOf('.')) + : _customSubnet; Future setFirstSubnet(int firstSubnet) async { _firstSubnet = firstSubnet; @@ -45,12 +54,25 @@ class AppSettings { .setInt(_pingCountKey, _pingCount); } + Future setInAppInternet(bool inAppInternet) async { + _inAppInternet = inAppInternet; + return (await SharedPreferences.getInstance()) + .setBool(_inAppInternetKey, _inAppInternet); + } + + Future setCustomSubnet(String customSubnet) async { + _customSubnet = customSubnet; + return (await SharedPreferences.getInstance()) + .setString(_customSubnetKey, _customSubnet); + } + Future load() async { debugPrint("Fetching all app settings"); _firstSubnet = (await SharedPreferences.getInstance()).getInt(_firstSubnetKey) ?? _firstSubnet; debugPrint("First subnet : $_firstSubnet"); + _lastSubnet = (await SharedPreferences.getInstance()).getInt(_lastSubnetKey) ?? _lastSubnet; @@ -60,10 +82,20 @@ class AppSettings { (await SharedPreferences.getInstance()).getInt(_socketTimeoutKey) ?? _socketTimeout; debugPrint("Socket timeout : $_socketTimeout"); + _pingCount = (await SharedPreferences.getInstance()).getInt(_pingCountKey) ?? _pingCount; + debugPrint("Ping count : $_pingCount"); + + _inAppInternet = + (await SharedPreferences.getInstance()).getBool(_inAppInternetKey) ?? + _inAppInternet; + debugPrint("In-App Internet : $_inAppInternet"); - debugPrint("Ping count : $_socketTimeout"); + _customSubnet = + (await SharedPreferences.getInstance()).getString(_customSubnetKey) ?? + _customSubnet; + debugPrint("Custom Subnet : $_customSubnet"); } } diff --git a/lib/helper/utils_helper.dart b/lib/helper/utils_helper.dart index 262cf8c9..b6500cd2 100644 --- a/lib/helper/utils_helper.dart +++ b/lib/helper/utils_helper.dart @@ -1,5 +1,16 @@ +import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:vernet/ui/external_link_dialog.dart'; Future launchURL(String url) async => await canLaunchUrlString(url) ? await launchUrlString(url) : throw 'Could not launch $url'; + +Future launchURLWithWarning(BuildContext context, String url) { + return showDialog( + context: context, + builder: (context) => ExternalLinkWarningDialog( + link: url, + ), + ); +} diff --git a/lib/main.dart b/lib/main.dart index 687cac74..90ee73aa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,7 +16,7 @@ Future main() async { configureDependencies(Env.prod); WidgetsFlutterBinding.ensureInitialized(); final appDocDirectory = await getApplicationDocumentsDirectory(); - await configureNetworkTools(appDocDirectory.path); + await configureNetworkToolsFlutter(appDocDirectory.path); final bool allowed = await ConsentLoader.isConsentPageShown(); // load app settings diff --git a/lib/models/wifi_info.dart b/lib/models/wifi_info.dart index d51620c7..966504d3 100644 --- a/lib/models/wifi_info.dart +++ b/lib/models/wifi_info.dart @@ -7,6 +7,20 @@ class WifiInfo { final String? _name; bool unknown; String get ip => _ip ?? 'x.x.x.x'; - String get name => _name ?? 'Wi-Fi'; + + static const String noWifiName = 'Wi-Fi'; + + String get name { + if (_name == null || _name!.isEmpty) return noWifiName; + if (_name!.startsWith('"') && _name!.endsWith('"')) { + final array = _name!.split('"'); + if (array.length > 1) { + final wifiName = array[1]; + return wifiName.isEmpty ? noWifiName : wifiName; + } + } + return _name!; + } + String get bssid => _bssid ?? defaultBSSID.first; } diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 4ffac460..32ed1032 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -5,6 +5,7 @@ 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/main.dart'; import 'package:vernet/models/internet_provider.dart'; import 'package:vernet/models/wifi_info.dart'; import 'package:vernet/pages/dns/dns_page.dart'; @@ -194,52 +195,58 @@ class _WifiDetailState extends State { subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - FutureBuilder( - future: ISPLoader().load(), - builder: ( - BuildContext context, - AsyncSnapshot snapshot, - ) { - if (snapshot.hasData && snapshot.data != null) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomTile( - leading: Icon( - Icons.public, - color: Theme.of(context).colorScheme.secondary, + if (appSettings.inAppInternet) + FutureBuilder( + future: ISPLoader().load(), + builder: ( + BuildContext context, + AsyncSnapshot snapshot, + ) { + if (snapshot.hasData && snapshot.data != null) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomTile( + leading: Icon( + Icons.public, + color: + Theme.of(context).colorScheme.secondary, + ), + child: Text(snapshot.data!.ip), ), - child: Text(snapshot.data!.ip), - ), - CustomTile( - leading: Icon( - Icons.dns, - color: Theme.of(context).colorScheme.secondary, + CustomTile( + leading: Icon( + Icons.dns, + color: + Theme.of(context).colorScheme.secondary, + ), + child: Text(snapshot.data!.isp), ), - child: Text(snapshot.data!.isp), - ), - CustomTile( - leading: Icon( - Icons.location_on, - color: Theme.of(context).colorScheme.secondary, + CustomTile( + leading: Icon( + Icons.location_on, + color: + Theme.of(context).colorScheme.secondary, + ), + child: Text(snapshot.data!.location.address), ), - child: Text(snapshot.data!.location.address), - ), - const SizedBox(height: 5), - const Divider(height: 3), - ], - ); - } - if (snapshot.hasError) { - return const Text('Unable to fetch ISP details'); - } - return const Text('Loading ISP details..'); - }, - ), + const SizedBox(height: 5), + const Divider(height: 3), + ], + ); + } + if (snapshot.hasError) { + return const Text('Unable to fetch ISP details'); + } + return const Text('Loading ISP details..'); + }, + ) + else + const Text("In-App Internet is off"), const SizedBox(height: 10), ElevatedButton.icon( onPressed: () { - launchURL('https://fast.com'); + launchURLWithWarning(context, 'https://fast.com'); }, icon: const Icon(Icons.speed), label: const Text('Speed Test'), diff --git a/lib/pages/host_scan_page/host_scna_bloc/host_scan_bloc.dart b/lib/pages/host_scan_page/host_scan_bloc/host_scan_bloc.dart similarity index 72% rename from lib/pages/host_scan_page/host_scna_bloc/host_scan_bloc.dart rename to lib/pages/host_scan_page/host_scan_bloc/host_scan_bloc.dart index c410bcb7..638f8e9d 100644 --- a/lib/pages/host_scan_page/host_scna_bloc/host_scan_bloc.dart +++ b/lib/pages/host_scan_page/host_scan_bloc/host_scan_bloc.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; +import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:injectable/injectable.dart'; import 'package:network_info_plus/network_info_plus.dart'; @@ -39,9 +40,10 @@ class HostScanBloc extends Bloc { ) async { emit(const HostScanState.loadInProgress()); ip = await NetworkInfo().getWifiIP(); - subnet = ip!.substring(0, ip!.lastIndexOf('.')); - gatewayIp = await NetworkInfo().getWifiGatewayIP(); - + gatewayIp = appSettings.customSubnet.isNotEmpty + ? appSettings.customSubnet + : await NetworkInfo().getWifiGatewayIP(); + subnet = gatewayIp!.substring(0, gatewayIp!.lastIndexOf('.')); add(const HostScanEvent.startNewScan()); } @@ -49,38 +51,7 @@ class HostScanBloc extends Bloc { StartNewScan event, Emitter emit, ) async { - MdnsScanner.searchMdnsDevices() - .then((List activeHostList) async { - for (final ActiveHost activeHost in activeHostList) { - 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)); - } - }); - - final streamController = HostScannerFlutter.getAllPingableDevices( + final streamController = HostScannerService.instance.getAllPingableDevices( subnet!, firstHostId: appSettings.firstSubnet, lastHostId: appSettings.lastSubnet, @@ -108,10 +79,40 @@ class HostScanBloc extends Bloc { } deviceInTheNetworkList.sort(sort); + emit(const HostScanState.loadInProgress()); + emit(HostScanState.foundNewDevice(deviceInTheNetworkList)); + } + 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)); } /// Getting active host IP and finds it's index inside of activeHostList diff --git a/lib/pages/host_scan_page/host_scna_bloc/host_scan_event.dart b/lib/pages/host_scan_page/host_scan_bloc/host_scan_event.dart similarity index 100% rename from lib/pages/host_scan_page/host_scna_bloc/host_scan_event.dart rename to lib/pages/host_scan_page/host_scan_bloc/host_scan_event.dart diff --git a/lib/pages/host_scan_page/host_scna_bloc/host_scan_state.dart b/lib/pages/host_scan_page/host_scan_bloc/host_scan_state.dart similarity index 100% rename from lib/pages/host_scan_page/host_scna_bloc/host_scan_state.dart rename to lib/pages/host_scan_page/host_scan_bloc/host_scan_state.dart diff --git a/lib/pages/host_scan_page/host_scan_page.dart b/lib/pages/host_scan_page/host_scan_page.dart index 2d246894..e40b6c58 100644 --- a/lib/pages/host_scan_page/host_scan_page.dart +++ b/lib/pages/host_scan_page/host_scan_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:vernet/injection.dart'; -import 'package:vernet/pages/host_scan_page/host_scna_bloc/host_scan_bloc.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'; class HostScanPage extends StatelessWidget { @@ -10,24 +10,6 @@ class HostScanPage extends StatelessWidget { return Scaffold( appBar: AppBar( title: const Text('Scan for Devices'), - // actions: [ - // if (_isScanning) - // Container( - // margin: const EdgeInsets.only(right: 20.0), - // child: CircularPercentIndicator( - // radius: 10.0, - // lineWidth: 2.5, - // percent: _progress / 100, - // backgroundColor: Colors.grey, - // progressColor: Colors.white, - // ), - // ) - // else - // IconButton( - // onPressed: _getDevices, - // icon: const Icon(Icons.refresh), - // ), - // ], ), body: BlocProvider( create: (context) => 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 49ed3d30..d0cc8d3a 100644 --- a/lib/pages/host_scan_page/widgets/host_scan_widget.dart +++ b/lib/pages/host_scan_page/widgets/host_scan_widget.dart @@ -1,10 +1,10 @@ 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/pages/host_scan_page/host_scna_bloc/host_scan_bloc.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/pages/port_scan_page/port_scan_page.dart'; class HostScanWidget extends StatelessWidget { @override @@ -17,16 +17,19 @@ class HostScanWidget extends StatelessWidget { return Center( child: Container( margin: const EdgeInsets.all(30), - child: const Column( + child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProgressIndicator(), - SizedBox( + const CircularProgressIndicator(), + const SizedBox( height: 30, ), Text( - 'Searching for devices in your local network', - style: TextStyle( + appSettings.gatewayIP.isNotEmpty + ? 'Searching for devices in ${appSettings.gatewayIP} network' + : 'Searching for devices in your local network', + textAlign: TextAlign.center, + style: const TextStyle( fontSize: 18, color: Colors.blue, ), @@ -37,71 +40,13 @@ class HostScanWidget extends StatelessWidget { ); }, foundNewDevice: (FoundNewDevice value) { - final List activeHostList = - value.activeHostList; - - return Flex( - direction: Axis.vertical, - children: [ - Padding( - padding: const EdgeInsets.all(4.0), - child: Text("Found ${activeHostList.length} devices"), - ), - Expanded( - child: ListView.builder( - itemCount: activeHostList.length, - itemBuilder: (context, index) { - final DeviceInTheNetwork host = activeHostList[index]; - return ListTile( - leading: Icon(host.iconData), - title: FutureBuilder( - future: host.make, - builder: (context, AsyncSnapshot snapshot) { - return Text(snapshot.data ?? ''); - }, - initialData: 'Generic Device', - ), - subtitle: Text( - '${host.internetAddress.address} ${host.mac}', - ), - trailing: IconButton( - tooltip: 'Scan open ports for this target', - icon: const Icon(Icons.radar), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PortScanPage( - target: host.internetAddress.address, - ), - ), - ); - }, - ), - onLongPress: () { - Clipboard.setData( - ClipboardData( - text: host.internetAddress.address, - ), - ); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('IP copied to clipboard'), - ), - ); - }, - ); - }, - ), - ), - ], - ); + return _devicesWidget(value.activeHostList); }, loadFailure: (value) { return const Text('Failure'); }, loadSuccess: (value) { - return const Text('Done'); + return _devicesWidget(value.activeHostList); }, error: (Error value) { return const Text('Error'); @@ -110,4 +55,63 @@ class HostScanWidget extends StatelessWidget { }, ); } + + Widget _devicesWidget(List activeHostList) { + return Flex( + direction: Axis.vertical, + children: [ + Padding( + padding: const EdgeInsets.all(4.0), + child: Text("Found ${activeHostList.length} devices"), + ), + Expanded( + child: ListView.builder( + itemCount: activeHostList.length, + itemBuilder: (context, index) { + final DeviceInTheNetwork host = activeHostList[index]; + return ListTile( + leading: Icon(host.iconData), + title: FutureBuilder( + future: host.make, + builder: (context, AsyncSnapshot snapshot) { + return Text(snapshot.data ?? ''); + }, + initialData: 'Generic Device', + ), + subtitle: Text( + '${host.internetAddress.address} ${host.mac}', + ), + trailing: IconButton( + tooltip: 'Scan open ports for this target', + icon: const Icon(Icons.radar), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PortScanPage( + target: host.internetAddress.address, + ), + ), + ); + }, + ), + onLongPress: () { + Clipboard.setData( + ClipboardData( + text: host.internetAddress.address, + ), + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('IP copied to clipboard'), + ), + ); + }, + ); + }, + ), + ), + ], + ); + } } diff --git a/lib/pages/network_troubleshoot/port_scan_page.dart b/lib/pages/network_troubleshoot/port_scan_page.dart index 6bd33565..a3534fe1 100644 --- a/lib/pages/network_troubleshoot/port_scan_page.dart +++ b/lib/pages/network_troubleshoot/port_scan_page.dart @@ -10,9 +10,10 @@ import 'package:vernet/ui/custom_tile.dart'; import 'package:vernet/ui/popular_chip.dart'; class PortScanPage extends StatefulWidget { - const PortScanPage({this.target = ''}); + const PortScanPage({this.target = '', this.runDefaultScan = false}); final String target; + final bool runDefaultScan; @override _PortScanPageState createState() => _PortScanPageState(); @@ -77,28 +78,35 @@ class _PortScanPageState extends State _openPorts.clear(); }); if (_type == ScanType.single) { - PortScannerFlutter.isOpen( + PortScannerService.instance + .isOpen( _targetIPEditingController.text, int.parse(_singlePortEditingController.text), - ).then((value) { + ) + .then((value) { _handleEvent(value); _handleOnDone(); }); } else if (_type == ScanType.top) { - _streamSubscription = PortScannerFlutter.customDiscover( - _targetIPEditingController.text, - timeout: Duration(milliseconds: appSettings.socketTimeout), - progressCallback: _handleProgress, - ).listen(_handleEvent, onDone: _handleOnDone); + _streamSubscription = PortScannerService.instance + .customDiscover( + _targetIPEditingController.text, + timeout: Duration(milliseconds: appSettings.socketTimeout), + progressCallback: _handleProgress, + async: true, + ) + .listen(_handleEvent, onDone: _handleOnDone); } else { - _streamSubscription = PortScannerFlutter.scanPortsForSingleDevice( - _targetIPEditingController.text, - startPort: int.parse(_startPortEditingController.text), - endPort: int.parse(_endPortEditingController.text), - timeout: Duration(milliseconds: appSettings.socketTimeout), - progressCallback: _handleProgress, - async: true, - ).listen(_handleEvent, onDone: _handleOnDone); + _streamSubscription = PortScannerService.instance + .scanPortsForSingleDevice( + _targetIPEditingController.text, + startPort: int.parse(_startPortEditingController.text), + endPort: int.parse(_endPortEditingController.text), + timeout: Duration(milliseconds: appSettings.socketTimeout), + progressCallback: _handleProgress, + async: true, + ) + .listen(_handleEvent, onDone: _handleOnDone); } } @@ -107,6 +115,9 @@ class _PortScanPageState extends State super.initState(); _tabController = TabController(length: _tabs.length, vsync: this); _targetIPEditingController.text = widget.target; + if (widget.runDefaultScan) { + Future.delayed(Durations.short2, _startScanning); + } } ScanType? _type = ScanType.top; diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 0668eee7..fe55062d 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -5,6 +5,7 @@ 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/ui/settings_dialog/custom_subnet_dialog.dart'; import 'package:vernet/ui/settings_dialog/first_subnet_dialog.dart'; import 'package:vernet/ui/settings_dialog/last_subnet_dialog.dart'; import 'package:vernet/ui/settings_dialog/ping_count_dialog.dart'; @@ -40,6 +41,19 @@ class _SettingsPageState extends State { }, ), ), + Card( + child: ListTile( + title: const Text('In-App Internet'), + trailing: Switch( + value: appSettings.inAppInternet, + onChanged: (bool? value) async { + appSettings.setInAppInternet(value ?? false); + await appSettings.load(); + setState(() {}); + }, + ), + ), + ), Card( child: ListTile( title: const Text(StringValue.firstSubnet), @@ -124,6 +138,27 @@ class _SettingsPageState extends State { }, ), ), + Card( + child: ListTile( + title: const Text(StringValue.customSubnet), + subtitle: const Text(StringValue.customSubnetDesc), + trailing: Text( + appSettings.customSubnet, + style: Theme.of(context) + .textTheme + .titleSmall + ?.copyWith(color: Theme.of(context).colorScheme.secondary), + ), + onTap: () async { + await showDialog( + context: context, + builder: (context) => const CustomSubnetDialog(), + ); + await appSettings.load(); + setState(() {}); + }, + ), + ), Card( child: ListTile( title: const Text('Check for Updates'), @@ -149,45 +184,23 @@ class _SettingsPageState extends State { ListTile( leading: const Icon(Icons.bug_report), title: const Text('Report Issues'), - // subtitle: Text(_issueUrl), - // trailing: IconButton( - // icon: Icon(Icons.open_in_new), - // onPressed: () { - // _launchURL(_issueUrl); - // }, - // ), onTap: () { - launchURL(_issueUrl); + launchURLWithWarning(context, _issueUrl); }, ), ListTile( leading: const Icon(Icons.favorite), title: const Text('Donate'), - // subtitle: Text(_donateUrl), - // trailing: IconButton( - // icon: Icon(Icons.open_in_new), - // onPressed: () { - // _launchURL(_donateUrl); - // }, - // ), onTap: () { - launchURL(_donateUrl); + launchURLWithWarning(context, _donateUrl); }, ), ListTile( leading: const Icon(Icons.code), title: const Text('Source Code'), - // subtitle: Text(_srcUrl), onTap: () { - launchURL(_srcUrl); + launchURLWithWarning(context, _srcUrl); }, - - // trailing: IconButton( - // icon: Icon(Icons.open_in_new), - // onPressed: () { - // _launchURL(_srcUrl); - // }, - // ), ), const ListTile( title: Text( diff --git a/lib/ui/external_link_dialog.dart b/lib/ui/external_link_dialog.dart new file mode 100644 index 00000000..aea870eb --- /dev/null +++ b/lib/ui/external_link_dialog.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:vernet/helper/utils_helper.dart'; + +class ExternalLinkWarningDialog extends StatelessWidget { + const ExternalLinkWarningDialog({super.key, required this.link}); + + final String link; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text("Confirm to open external link"), + content: Text(link), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Cancel'), + ), + TextButton.icon( + onPressed: () { + launchURL(link); + }, + icon: const Icon(Icons.link), + label: const Text('Open Link'), + ), + ], + ); + } +} diff --git a/lib/ui/settings_dialog/custom_subnet_dialog.dart b/lib/ui/settings_dialog/custom_subnet_dialog.dart new file mode 100644 index 00000000..ff9b45a1 --- /dev/null +++ b/lib/ui/settings_dialog/custom_subnet_dialog.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:vernet/main.dart'; +import 'package:vernet/ui/base_settings_dialog.dart'; +import 'package:vernet/values/strings.dart'; + +class CustomSubnetDialog extends StatefulWidget { + const CustomSubnetDialog({super.key}); + + @override + State createState() => _CustomSubnetDialogState(); +} + +class _CustomSubnetDialogState extends BaseSettingsDialog { + @override + String getDialogTitle() { + return StringValue.customSubnet; + } + + @override + String getHintText() { + return StringValue.customSubnetHint; + } + + @override + String getInitialValue() { + return appSettings.customSubnet; + } + + @override + TextInputType getKeyBoardType() { + return TextInputType.number; + } + + @override + void onSubmit(String value) { + if (value != appSettings.customSubnet) { + appSettings.setCustomSubnet(value); + } + } + + @override + String? validate(String? value) { + return null; + } +} diff --git a/lib/ui/settings_dialog/internet_dialog.dart b/lib/ui/settings_dialog/internet_dialog.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/values/strings.dart b/lib/values/strings.dart index 11d41756..d8607c3c 100644 --- a/lib/values/strings.dart +++ b/lib/values/strings.dart @@ -14,4 +14,9 @@ class StringValue { static const String pingCount = 'Ping Count'; static const String pingCountDesc = 'Number of times ping request should be sent'; + + static const String customSubnet = 'Custom Subnet'; + static const String customSubnetDesc = + 'Scan a custom subnet instead of local one.'; + static const String customSubnetHint = 'Enter Gateway IP e.g., 10.102.200.1'; } diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 36107ed8..2be7bb43 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -203,7 +203,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3bbcb37d..198e90cf 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =2.17.0 <3.0.0" @@ -17,6 +17,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.5 dart_ping: ^9.0.0 + external_app_launcher: ^3.1.0 flutter: sdk: flutter # Bloc for state management, replace StatefulWidget @@ -34,7 +35,7 @@ dependencies: # Discover network info and configure themselves accordingly network_info_plus: ^4.0.2 # Helps you discover open ports, devices on subnet and more. - network_tools_flutter: ^1.0.5 + network_tools_flutter: ^2.0.0 # Querying information about the application package, such as CFBundleVersion package_info_plus: ^4.1.0 path_provider: ^2.1.1