diff --git a/.vscode/settings.json b/.vscode/settings.json index 9ddf6b2..34096c2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "cmake.ignoreCMakeListsMissing": true + "cmake.ignoreCMakeListsMissing": true, + "java.configuration.updateBuildConfiguration": "disabled" } \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e875a60..91dfe0d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,8 @@ - + + + - - - - + \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 71364bc..881d5bf 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -45,5 +45,8 @@ UIApplicationSupportsIndirectInputEvents + NSFaceIDUsageDescription + QuickSSH needs FaceID to protect your terminal access. + diff --git a/lib/main.dart b/lib/main.dart index 2f3ef60..31aa622 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,7 +7,13 @@ import 'package:QuickSSH/services/theme_controller.dart'; final themeController = ThemeController(); final settingsController = SettingsController(); -void main() { +final GlobalKey navigatorKey = GlobalKey(); + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + await settingsController.initializationFuture; + runApp(MyApp()); } @@ -22,6 +28,8 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Quick SSH', + navigatorKey: navigatorKey, + theme: MyThemes.lightTheme, darkTheme: MyThemes.darkTheme, themeMode: themeController.themeMode, diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 4c58853..5a7407b 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -5,6 +5,9 @@ import 'add_client.dart'; import 'package:QuickSSH/classes/ServerCommand.dart'; import 'package:QuickSSH/services/storage_service.dart'; import 'package:QuickSSH/screens/settings.dart'; +import 'package:flutter/services.dart'; +import 'package:QuickSSH/main.dart'; +import 'package:QuickSSH/widgets/ad_banner.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -20,6 +23,13 @@ class _HomeScreenState extends State { void initState() { super.initState(); _loadData(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + bool allowed = await settingsController.checkSecurity(); + + if (!allowed) { + SystemNavigator.pop(); + } + }); } void _loadData() async { @@ -106,6 +116,9 @@ class _HomeScreenState extends State { ); }, ), + bottomNavigationBar: SafeArea( + child: MyBannerAd(isEnabled: settingsController.adsEnabled), + ), backgroundColor: theme.surface, ); } @@ -151,7 +164,9 @@ class _HomeScreenState extends State { _openSettings() { Navigator.push( context, - MaterialPageRoute(builder: (context) => const Settings()), - ); + MaterialPageRoute(builder: (context) => Settings()), + ).then((_) { + setState(() {}); + }); } } diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index d84b621..128587f 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -115,6 +115,64 @@ class _SettingsState extends State { ), ], ), + SizedBox(height: 20), + Row( + children: [ + Expanded( + child: Text( + "Biometric Unlock", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + color: theme.onSurface, + ), + ), + ), + Expanded( + child: Switch( + activeTrackColor: theme.primary, + activeThumbColor: theme.onPrimary, + inactiveThumbColor: theme.onSurface, + inactiveTrackColor: theme.surface, + value: settingsController.biometricEnabled, + onChanged: (bool value) { + settingsController.toggleBiometric(value); + if (value) HapticFeedback.mediumImpact(); + setState(() {}); + }, + ), + ), + ], + ), + SizedBox(height: 20), + Row( + children: [ + Expanded( + child: Text( + "Enable Ads", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + color: theme.onSurface, + ), + ), + ), + Expanded( + child: Switch( + activeTrackColor: theme.primary, + activeThumbColor: theme.onPrimary, + inactiveThumbColor: theme.onSurface, + inactiveTrackColor: theme.surface, + value: settingsController.adsEnabled, + onChanged: (bool value) { + settingsController.toggleAds(value); + if (value) HapticFeedback.mediumImpact(); + setState(() {}); + }, + ), + ), + ], + ), ], ), ), diff --git a/lib/services/settings_controller.dart b/lib/services/settings_controller.dart index 3d2a9af..807116e 100644 --- a/lib/services/settings_controller.dart +++ b/lib/services/settings_controller.dart @@ -1,25 +1,71 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:local_auth/local_auth.dart'; class SettingsController extends ChangeNotifier { bool _vibrationEnabled = true; bool get vibrationEnabled => _vibrationEnabled; + bool _biometricEnabled = false; + bool get biometricEnabled => _biometricEnabled; + + bool _adsEnabled = true; + bool get adsEnabled => _adsEnabled; + + final LocalAuthentication auth = LocalAuthentication(); + + late Future initializationFuture; + SettingsController() { - _loadSettings(); + initializationFuture = _loadSettings(); } Future _loadSettings() async { final prefs = await SharedPreferences.getInstance(); _vibrationEnabled = prefs.getBool('vibration_enabled') ?? true; + _biometricEnabled = prefs.getBool('biometric_enabled') ?? false; + _adsEnabled = prefs.getBool('ads_enabled') ?? true; notifyListeners(); } Future toggleVibration(bool value) async { - print("Toggling vibration to: $value"); _vibrationEnabled = value; final prefs = await SharedPreferences.getInstance(); await prefs.setBool('vibration_enabled', value); notifyListeners(); } + + Future toggleBiometric(bool value) async { + _biometricEnabled = value; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('biometric_enabled', value); + notifyListeners(); + } + + Future toggleAds(bool value) async { + _adsEnabled = value; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('ads_enabled', value); + notifyListeners(); + } + + Future checkSecurity() async { + await initializationFuture; + + if (!_biometricEnabled) return true; + + bool canCheck = await auth.canCheckBiometrics; + bool isSupported = await auth.isDeviceSupported(); + + if (!canCheck && !isSupported) return true; + + try { + return await auth.authenticate( + localizedReason: 'Unlock QuickSSH', + biometricOnly: false, + ); + } catch (e) { + return false; + } + } } diff --git a/lib/widgets/ad_banner.dart b/lib/widgets/ad_banner.dart new file mode 100644 index 0000000..8f1bdfd --- /dev/null +++ b/lib/widgets/ad_banner.dart @@ -0,0 +1,58 @@ +import 'package:google_mobile_ads/google_mobile_ads.dart'; +import 'package:flutter/material.dart'; + +class MyBannerAd extends StatefulWidget { + final bool isEnabled; + const MyBannerAd({super.key, required this.isEnabled}); + + @override + State createState() => _MyBannerAdState(); +} + +class _MyBannerAdState extends State { + BannerAd? _bannerAd; + bool _isLoaded = false; + bool _isInitializing = false; + + @override + void initState() { + super.initState(); + if (widget.isEnabled) { + _loadAd(); + } + } + + void _loadAd() async { + _bannerAd = BannerAd( + adUnitId: 'ca-app-pub-2626773788355001/7557216229', + request: const AdRequest(), + size: AdSize.banner, + listener: BannerAdListener( + onAdLoaded: (ad) => setState(() => _isLoaded = true), + onAdFailedToLoad: (ad, error) { + ad.dispose(); + print('Ad failed to load: $error'); + }, + ), + )..load(); + } + + @override + void dispose() { + _bannerAd?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (!widget.isEnabled || !_isLoaded || _bannerAd == null) { + return const SizedBox.shrink(); + } + return Container( + alignment: Alignment.center, + width: double.infinity, + height: _bannerAd!.size.height.toDouble(), + child: AdWidget(ad: _bannerAd!), + ); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 37cc413..4daa34a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,10 +9,12 @@ import flutter_secure_storage_macos import local_auth_darwin import path_provider_foundation import shared_preferences_foundation +import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 9433154..fa3fe6f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -224,6 +224,14 @@ packages: description: flutter source: sdk version: "0.0.0" + google_mobile_ads: + dependency: "direct main" + description: + name: google_mobile_ads + sha256: "0d4a3744b5e8ed1b8be6a1b452d309f811688855a497c6113fc4400f922db603" + url: "https://pub.dev" + source: hosted + version: "5.3.1" image: dependency: transitive description: @@ -292,26 +300,26 @@ packages: dependency: "direct main" description: name: local_auth - sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b" + sha256: a4f1bf57f0236a4aeb5e8f0ec180e197f4b112a3456baa6c1e73b546630b0422 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "3.0.0" local_auth_android: dependency: transitive description: name: local_auth_android - sha256: a0bdfcc0607050a26ef5b31d6b4b254581c3d3ce3c1816ab4d4f4a9173e84467 + sha256: "162b8e177fd9978c4620da2a8002a5c6bed4d20f0c6daf5137e72e9a8b767d2e" url: "https://pub.dev" source: hosted - version: "1.0.56" + version: "2.0.4" local_auth_darwin: dependency: transitive description: name: local_auth_darwin - sha256: "699873970067a40ef2f2c09b4c72eb1cfef64224ef041b3df9fdc5c4c1f91f49" + sha256: "668ea65edaab17380956e9713f57e34f78ede505ca0cfd8d39db34e2f260bfee" url: "https://pub.dev" source: hosted - version: "1.6.1" + version: "2.0.1" local_auth_platform_interface: dependency: transitive description: @@ -324,10 +332,10 @@ packages: dependency: transitive description: name: local_auth_windows - sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5 + sha256: be12c5b8ba5e64896983123655c5f67d2484ecfcc95e367952ad6e3bff94cb16 url: "https://pub.dev" source: hosted - version: "1.0.11" + version: "2.0.1" matcher: dependency: transitive description: @@ -597,6 +605,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + webview_flutter: + dependency: transitive + description: + name: webview_flutter + sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9 + url: "https://pub.dev" + source: hosted + version: "4.13.1" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: eeeb3fcd5f0ff9f8446c9f4bbc18a99b809e40297528a3395597d03aafb9f510 + url: "https://pub.dev" + source: hosted + version: "4.10.11" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" + url: "https://pub.dev" + source: hosted + version: "2.14.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: "0412b657a2828fb301e73509909e6ec02b77cd2b441ae9f77125d482b3ddf0e7" + url: "https://pub.dev" + source: hosted + version: "3.23.6" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f417d4e..c9bc01c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,7 +33,8 @@ dependencies: shared_preferences: ^2.5.0 dartssh2: ^2.0.0 flutter_secure_storage: ^9.2.2 - local_auth: ^2.3.0 + local_auth: ^3.0.0 + google_mobile_ads: ^5.1.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons.