diff --git a/lib/base.dart b/lib/base.dart index 6d4c6dc..c309385 100644 --- a/lib/base.dart +++ b/lib/base.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:animations/animations.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter/services.dart'; import 'package:adguard_home_manager/widgets/bottom_nav_bar.dart'; -import 'package:flutter/services.dart'; + +import 'package:adguard_home_manager/models/app_screen.dart'; +import 'package:adguard_home_manager/config/app_screens.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; class Base extends StatefulWidget { const Base({Key? key}) : super(key: key); @@ -16,6 +20,16 @@ class _BaseState extends State { @override Widget build(BuildContext context) { + final serversProvider = Provider.of(context); + + List screens = serversProvider.selectedServer != null + ? screensServerConnected + : screensSelectServer; + + if (selectedScreen > screens.length-1) { + setState(() => selectedScreen = 0); + } + return AnnotatedRegion( value: SystemUiOverlayStyle( statusBarColor: Colors.transparent, @@ -31,20 +45,14 @@ class _BaseState extends State { : Brightness.light, ), child: Scaffold( - body: PageTransitionSwitcher( - duration: const Duration(milliseconds: 200), - transitionBuilder: ( - (child, primaryAnimation, secondaryAnimation) => FadeThroughTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - child: child, - ) - ), - ), + appBar: screens[selectedScreen].appBar, + body: screens[selectedScreen].body, bottomNavigationBar: BottomNavBar( + screens: screens, selectedScreen: selectedScreen, onSelect: (value) => setState(() => selectedScreen = value), ), + floatingActionButton: screens[selectedScreen].fab, ), ); } diff --git a/lib/config/app_screens.dart b/lib/config/app_screens.dart index a3e1215..a0dad14 100644 --- a/lib/config/app_screens.dart +++ b/lib/config/app_screens.dart @@ -1,19 +1,42 @@ import 'package:flutter/material.dart'; -import 'package:adguard_home_manager/screens/home.dart'; -import 'package:adguard_home_manager/screens/settings.dart'; +import 'package:adguard_home_manager/screens/connect/fab.dart'; +import 'package:adguard_home_manager/screens/home/appbar.dart'; +import 'package:adguard_home_manager/screens/connect/appbar.dart'; +import 'package:adguard_home_manager/screens/connect/connect.dart'; +import 'package:adguard_home_manager/screens/home/home.dart'; +import 'package:adguard_home_manager/screens/settings/appbar.dart'; +import 'package:adguard_home_manager/screens/settings/settings.dart'; import 'package:adguard_home_manager/models/app_screen.dart'; -const List screens = [ - AppScreen( - name: "home", - icon: Icons.home_rounded, - widget: Home() +List screensSelectServer = [ + const AppScreen( + name: "connect", + icon: Icons.link_rounded, + appBar: ConnectAppBar(), + body: Connect(), + fab: FabConnect() ), - AppScreen( + const AppScreen( name: "settings", icon: Icons.settings_rounded, - widget: Settings() + appBar: SettingsAppBar(), + body: Settings() + ) +]; + +List screensServerConnected = [ + const AppScreen( + name: "home", + icon: Icons.home_rounded, + appBar: HomeAppBar(), + body: Home() + ), + const AppScreen( + name: "settings", + icon: Icons.settings_rounded, + appBar: SettingsAppBar(), + body: Settings() ) ]; \ No newline at end of file diff --git a/lib/config/system_overlay_style.dart b/lib/config/system_overlay_style.dart new file mode 100644 index 0000000..0f2e2e1 --- /dev/null +++ b/lib/config/system_overlay_style.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +SystemUiOverlayStyle systemUiOverlayStyleConfig(BuildContext context) => SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: Theme.of(context).brightness == Brightness.light + ? Brightness.light + : Brightness.dark, + statusBarIconBrightness: Theme.of(context).brightness == Brightness.light + ? Brightness.dark + : Brightness.light, + systemNavigationBarColor: Theme.of(context).scaffoldBackgroundColor, + systemNavigationBarIconBrightness: Theme.of(context).brightness == Brightness.light + ? Brightness.dark + : Brightness.light, +); \ No newline at end of file diff --git a/lib/functions/conversions.dart b/lib/functions/conversions.dart new file mode 100644 index 0000000..dd1e471 --- /dev/null +++ b/lib/functions/conversions.dart @@ -0,0 +1,45 @@ +import 'package:intl/intl.dart'; + +bool? convertFromIntToBool(int value) { + if (value == 1) { + return true; + } + else if (value == 0) { + return false; + } + else { + return null; + } +} + +int? convertFromBoolToInt(bool value) { + if (value == true) { + return 1; + } + else if (value == false) { + return 0; + } + else { + return null; + } +} + +double formatPercentage(double value) { + return (value * 100).truncateToDouble() / 100; +} + +String intFormat(int value, String locale) { + final f = NumberFormat("#,###", locale); + return f.format(value); +} + +List> convertFromMapToList(Map values) { + List> items = []; + values.forEach((key, value) { + items.add({ + "label": key, + "value": value + }); + }); + return items; +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d625271..4172e44 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,4 +1,18 @@ { "home": "Home", - "settings": "Settings" + "settings": "Settings", + "connect": "Connect", + "servers": "Servers", + "createConnection": "Create connection", + "name": "Name", + "ipDomain": "IP address or domain", + "path": "Path", + "port": "Port", + "username": "Username", + "password": "Password", + "defaultServer": "Default server", + "general": "General", + "connection": "Connection", + "authentication": "Authentication", + "other": "Other" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 3b2f032..ac3d3b9 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1,4 +1,18 @@ { "home": "Inicio", - "settings": "Ajustes" + "settings": "Ajustes", + "connect": "Conectar", + "servers": "Servidores", + "createConnection": "Crear conexión", + "name": "Nombre", + "ipDomain": "Dirección IP o dominio", + "path": "Ruta", + "port": "Puerto", + "username": "Nombre de usuario", + "password": "Contraseña", + "defaultServer": "Servidor por defecto", + "general": "General", + "connection": "Conexión", + "authentication": "Autenticación", + "other": "Otros" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 9235bef..39dc802 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,7 +11,9 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/base.dart'; +import 'package:adguard_home_manager/services/database.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/config/theme.dart'; @@ -22,6 +24,12 @@ void main() async { ); AppConfigProvider appConfigProvider = AppConfigProvider(); + ServersProvider serversProvider = ServersProvider(); + + final dbData = await loadDb(); + serversProvider.setDbInstance(dbData['dbInstance']); + appConfigProvider.setDbInstance(dbData['dbInstance']); + serversProvider.saveFromDb(dbData['servers']); PackageInfo appInfo = await PackageInfo.fromPlatform(); appConfigProvider.setAppInfo(appInfo); @@ -39,9 +47,12 @@ void main() async { runApp( MultiProvider( providers: [ + ChangeNotifierProvider( + create: ((context) => serversProvider) + ), ChangeNotifierProvider( create: ((context) => appConfigProvider) - ) + ), ], child: const Main(), ) diff --git a/lib/models/app_screen.dart b/lib/models/app_screen.dart index 0de75c2..bfe70ad 100644 --- a/lib/models/app_screen.dart +++ b/lib/models/app_screen.dart @@ -3,11 +3,15 @@ import 'package:flutter/material.dart'; class AppScreen { final String name; final IconData icon; - final Widget widget; + final PreferredSizeWidget appBar; + final Widget body; + final Widget? fab; const AppScreen({ required this.name, required this.icon, - required this.widget + required this.appBar, + required this.body, + this.fab }); } \ No newline at end of file diff --git a/lib/models/server.dart b/lib/models/server.dart new file mode 100644 index 0000000..13e3d1b --- /dev/null +++ b/lib/models/server.dart @@ -0,0 +1,21 @@ +class Server { + final String name; + final String connectionMethod; + final String domain; + final String? path; + final int? port; + final String user; + final String password; + final bool defaultServer; + + const Server({ + required this.name, + required this.connectionMethod, + required this.domain, + this.path, + this.port, + required this.user, + required this.password, + required this.defaultServer + }); +} \ No newline at end of file diff --git a/lib/providers/app_config_provider.dart b/lib/providers/app_config_provider.dart index c8ec448..1f96839 100644 --- a/lib/providers/app_config_provider.dart +++ b/lib/providers/app_config_provider.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:sqflite/sqlite_api.dart'; class AppConfigProvider with ChangeNotifier { + Database? _dbInstance; + PackageInfo? _appInfo; AndroidDeviceInfo? _androidDeviceInfo; IosDeviceInfo? _iosDeviceInfo; @@ -19,6 +22,10 @@ class AppConfigProvider with ChangeNotifier { return _iosDeviceInfo; } + void setDbInstance(Database db) { + _dbInstance = db; + } + void setAppInfo(PackageInfo appInfo) { _appInfo = appInfo; } diff --git a/lib/providers/servers_provider.dart b/lib/providers/servers_provider.dart new file mode 100644 index 0000000..b1cca2f --- /dev/null +++ b/lib/providers/servers_provider.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:sqflite/sqflite.dart'; + +import 'package:adguard_home_manager/functions/conversions.dart'; +import 'package:adguard_home_manager/models/server.dart'; + +class ServersProvider with ChangeNotifier { + Database? _dbInstance; + + List _serversList = []; + Server? _selectedServer; + + List get serversList { + return _serversList; + } + + Server? get selectedServer { + return _selectedServer; + } + + void setDbInstance(Database db) { + _dbInstance = db; + } + + void addServer(Server server) { + _serversList.add(server); + notifyListeners(); + } + + void saveFromDb(List>? data) async { + if (data != null) { + for (var server in data) { + final Server serverObj = Server( + name: server['name'], + connectionMethod: server['connectionMethod'], + domain: server['domain'], + path: server['path'], + port: server['port'], + user: server['user'], + password: server['password'], + defaultServer: convertFromIntToBool(server['isDefaultServer'])!, + ); + _serversList.add(serverObj); + if (convertFromIntToBool(server['isDefaultServer']) == true) { + _selectedServer = serverObj; + } + } + } + notifyListeners(); + } +} \ No newline at end of file diff --git a/lib/screens/connect/appbar.dart b/lib/screens/connect/appbar.dart new file mode 100644 index 0000000..f805265 --- /dev/null +++ b/lib/screens/connect/appbar.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class ConnectAppBar extends StatelessWidget with PreferredSizeWidget { + const ConnectAppBar({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppBar( + title: Text(AppLocalizations.of(context)!.connect), + centerTitle: true, + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} \ No newline at end of file diff --git a/lib/screens/connect/connect.dart b/lib/screens/connect/connect.dart new file mode 100644 index 0000000..888c8b2 --- /dev/null +++ b/lib/screens/connect/connect.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class Connect extends StatelessWidget { + const Connect({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container(); + } +} \ No newline at end of file diff --git a/lib/screens/connect/fab.dart b/lib/screens/connect/fab.dart new file mode 100644 index 0000000..0c3204d --- /dev/null +++ b/lib/screens/connect/fab.dart @@ -0,0 +1,23 @@ +import 'package:adguard_home_manager/widgets/add_server_modal.dart'; +import 'package:flutter/material.dart'; + +class FabConnect extends StatelessWidget { + const FabConnect({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + void openAddServerModal() async { + await Future.delayed(const Duration(seconds: 0), (() => { + Navigator.push(context, MaterialPageRoute( + fullscreenDialog: true, + builder: (BuildContext context) => const AddServerModal() + )) + })); + } + + return FloatingActionButton( + onPressed: openAddServerModal, + child: const Icon(Icons.add_rounded), + ); + } +} \ No newline at end of file diff --git a/lib/screens/home/appbar.dart b/lib/screens/home/appbar.dart new file mode 100644 index 0000000..b54fba1 --- /dev/null +++ b/lib/screens/home/appbar.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class HomeAppBar extends StatelessWidget with PreferredSizeWidget { + const HomeAppBar({Key? key}) : super(key: key); + + @override + PreferredSizeWidget build(BuildContext context) { + return AppBar( + title: Text(AppLocalizations.of(context)!.home), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} \ No newline at end of file diff --git a/lib/screens/home.dart b/lib/screens/home/home.dart similarity index 100% rename from lib/screens/home.dart rename to lib/screens/home/home.dart diff --git a/lib/screens/settings/appbar.dart b/lib/screens/settings/appbar.dart new file mode 100644 index 0000000..5d91161 --- /dev/null +++ b/lib/screens/settings/appbar.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class SettingsAppBar extends StatelessWidget with PreferredSizeWidget { + const SettingsAppBar({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppBar( + title: Text(AppLocalizations.of(context)!.settings), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} \ No newline at end of file diff --git a/lib/screens/settings.dart b/lib/screens/settings/settings.dart similarity index 100% rename from lib/screens/settings.dart rename to lib/screens/settings/settings.dart diff --git a/lib/services/database.dart b/lib/services/database.dart new file mode 100644 index 0000000..d698499 --- /dev/null +++ b/lib/services/database.dart @@ -0,0 +1,35 @@ +import 'package:sqflite/sqflite.dart'; + +Future> loadDb() async { + List>? servers; + // List>? appConfig; + + Database db = await openDatabase( + 'adguard_home_manager.db', + version: 1, + onCreate: (Database db, int version) async { + await db.execute("CREATE TABLE servers (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, connectionMethod TEXT, domain TEXT, path TEXT, port INTEGER, user TEXT, password TEXT, defaultServer INTEGER)"); + }, + onUpgrade: (Database db, int oldVersion, int newVersion) async { + + }, + onOpen: (Database db) async { + await db.transaction((txn) async{ + servers = await txn.rawQuery( + 'SELECT * FROM servers', + ); + }); + // await db.transaction((txn) async{ + // appConfig = await txn.rawQuery( + // 'SELECT * FROM appConfig', + // ); + // }); + } + ); + + return { + "servers": servers, + // "appConfig": appConfig![0], + "dbInstance": db, + }; +} \ No newline at end of file diff --git a/lib/widgets/add_server_modal.dart b/lib/widgets/add_server_modal.dart new file mode 100644 index 0000000..09b412c --- /dev/null +++ b/lib/widgets/add_server_modal.dart @@ -0,0 +1,194 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/widgets/custom_radio_toggle.dart'; + +import 'package:adguard_home_manager/config/system_overlay_style.dart'; + +class AddServerModal extends StatefulWidget { + const AddServerModal({Key? key}) : super(key: key); + + @override + State createState() => _AddServerModalState(); +} + +class _AddServerModalState extends State { + final TextEditingController nameController = TextEditingController(); + + String connectionType = "http"; + + final TextEditingController ipDomainController = TextEditingController(); + String? ipDomainError; + + final TextEditingController pathController = TextEditingController(); + String? pathError; + + final TextEditingController portController = TextEditingController(); + String? portError; + + final TextEditingController userController = TextEditingController(); + + final TextEditingController passwordController = TextEditingController(); + + bool defaultServer = false; + + Widget sectionLabel(String label) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 30 + ), + child: Text( + label, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ); + } + + Widget textField({ + required String label, + required TextEditingController controller, + String? error, + required IconData icon, + TextInputType? keyboardType + }) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: TextFormField( + controller: controller, + decoration: InputDecoration( + prefixIcon: Icon(icon), + errorText: error, + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: label, + ), + keyboardType: keyboardType, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Scaffold( + backgroundColor: Theme.of(context).dialogBackgroundColor, + appBar: AppBar( + systemOverlayStyle: systemUiOverlayStyleConfig(context), + title: Text(AppLocalizations.of(context)!.createConnection), + elevation: 5, + actions: [ + Padding( + padding: const EdgeInsets.only(right: 10), + child: IconButton( + tooltip: AppLocalizations.of(context)!.connect, + onPressed: () => {}, + icon: const Icon(Icons.login_rounded) + ), + ), + ], + toolbarHeight: 70, + ), + body: ListView( + children: [ + sectionLabel(AppLocalizations.of(context)!.general), + textField( + label: AppLocalizations.of(context)!.name, + controller: nameController, + icon: Icons.badge_rounded + ), + sectionLabel(AppLocalizations.of(context)!.connection), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + CustomRadioToggle( + groupSelected: connectionType, + value: 'http', + label: 'HTTP', + onTap: (value) => setState(() => connectionType = value) + ), + CustomRadioToggle( + groupSelected: connectionType, + value: 'https', + label: 'HTTPS', + onTap: (value) => setState(() => connectionType = value) + ), + ], + ), + const SizedBox(height: 20), + textField( + label: AppLocalizations.of(context)!.ipDomain, + controller: ipDomainController, + icon: Icons.link_rounded, + error: ipDomainError, + keyboardType: TextInputType.url + ), + const SizedBox(height: 20), + textField( + label: AppLocalizations.of(context)!.path, + controller: pathController, + icon: Icons.route_rounded, + error: pathError + ), + const SizedBox(height: 20), + textField( + label: AppLocalizations.of(context)!.port, + controller: portController, + icon: Icons.numbers_rounded, + error: portError, + keyboardType: TextInputType.number + ), + sectionLabel(AppLocalizations.of(context)!.authentication), + textField( + label: AppLocalizations.of(context)!.username, + controller: userController, + icon: Icons.person_rounded, + ), + const SizedBox(height: 20), + textField( + label: AppLocalizations.of(context)!.password, + controller: passwordController, + icon: Icons.lock_rounded, + keyboardType: TextInputType.visiblePassword + ), + sectionLabel(AppLocalizations.of(context)!.other), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => setState(() => defaultServer = !defaultServer), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AppLocalizations.of(context)!.defaultServer, + style: const TextStyle( + fontSize: 15, + ), + ), + Switch( + value: defaultServer, + onChanged: (value) => setState(() => defaultServer = value), + activeColor: Theme.of(context).primaryColor, + ) + ], + ), + ), + ), + ), + const SizedBox(height: 20), + ], + ), + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/widgets/bottom_nav_bar.dart b/lib/widgets/bottom_nav_bar.dart index c25e523..4475eeb 100644 --- a/lib/widgets/bottom_nav_bar.dart +++ b/lib/widgets/bottom_nav_bar.dart @@ -1,14 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:adguard_home_manager/models/app_screen.dart'; import 'package:adguard_home_manager/config/app_screens.dart'; class BottomNavBar extends StatelessWidget { + final List screens; final int selectedScreen; final void Function(int) onSelect; const BottomNavBar({ - Key? key, + Key? key, + required this.screens, required this.selectedScreen, required this.onSelect, }) : super(key: key); @@ -23,6 +26,9 @@ class BottomNavBar extends StatelessWidget { case 'settings': return AppLocalizations.of(context)!.settings; + case 'connect': + return AppLocalizations.of(context)!.connect; + default: return ''; } diff --git a/lib/widgets/custom_radio_toggle.dart b/lib/widgets/custom_radio_toggle.dart new file mode 100644 index 0000000..d745020 --- /dev/null +++ b/lib/widgets/custom_radio_toggle.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +class CustomRadioToggle extends StatelessWidget { + final String groupSelected; + final String value; + final String label; + final void Function(String) onTap; + + const CustomRadioToggle({ + Key? key, + required this.groupSelected, + required this.value, + required this.label, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + borderRadius: BorderRadius.circular(30), + color: Colors.transparent, + child: InkWell( + onTap: () => onTap(value), + borderRadius: BorderRadius.circular(30), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 5 + ), + decoration: BoxDecoration( + color: groupSelected == value + ? Theme.of(context).primaryColor + : Theme.of(context).primaryColor.withOpacity(0.05), + border: Border.all( + color: Theme.of(context).primaryColor + ), + borderRadius: BorderRadius.circular(30) + ), + child: Row( + children: [ + if (groupSelected == value) ...[ + const Icon( + Icons.check, + color: Colors.white, + size: 18, + ), + const SizedBox(width: 10), + ], + Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: groupSelected == value + ? Colors.white + : null + ), + ) + ], + ), + ), + ), + ); + } +} \ No newline at end of file