diff --git a/assets/icon/icon1024-white-center.png b/assets/icon/icon1024-white-center.png new file mode 100755 index 0000000..a7917e9 Binary files /dev/null and b/assets/icon/icon1024-white-center.png differ diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 93898a1..930685c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -55,5 +55,11 @@ "topQueriedDomains": "Top queried domains", "viewMore": "View more", "topClients": "Top clients", - "topBlockedDomains": "Top blocked domains" + "topBlockedDomains": "Top blocked domains", + "appSettings": "App settings", + "theme": "Theme", + "light": "Light", + "dark": "Dark", + "systemDefined": "System defined", + "close": "Close" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 3bd5bf1..fa946be 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -55,5 +55,11 @@ "topQueriedDomains": "Top dominios solicitados", "viewMore": "Ver más", "topClients": "Top clientes recurrentes", - "topBlockedDomains": "Top dominios bloqueados" + "topBlockedDomains": "Top dominios bloqueados", + "appSettings": "Ajustes de la app", + "theme": "Tema", + "light": "Claro", + "dark": "Oscuro", + "systemDefined": "Definido por el sistema", + "close": "Cerrar" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 39dc802..031f7c3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -28,7 +28,7 @@ void main() async { final dbData = await loadDb(); serversProvider.setDbInstance(dbData['dbInstance']); - appConfigProvider.setDbInstance(dbData['dbInstance']); + appConfigProvider.saveFromDb(dbData['dbInstance'], dbData['appConfig']); serversProvider.saveFromDb(dbData['servers']); PackageInfo appInfo = await PackageInfo.fromPlatform(); @@ -75,6 +75,7 @@ class Main extends StatelessWidget { darkTheme: appConfigProvider.androidDeviceInfo != null && appConfigProvider.androidDeviceInfo!.version.sdkInt! >= 31 ? darkTheme(darkDynamic) : darkThemeOldVersions(), + themeMode: appConfigProvider.selectedTheme, debugShowCheckedModeBanner: false, localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, diff --git a/lib/providers/app_config_provider.dart b/lib/providers/app_config_provider.dart index 1f96839..2700ef1 100644 --- a/lib/providers/app_config_provider.dart +++ b/lib/providers/app_config_provider.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/scheduler.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:sqflite/sqlite_api.dart'; @@ -10,6 +11,8 @@ class AppConfigProvider with ChangeNotifier { AndroidDeviceInfo? _androidDeviceInfo; IosDeviceInfo? _iosDeviceInfo; + int _selectedTheme = 0; + PackageInfo? get getAppInfo { return _appInfo; } @@ -22,6 +25,29 @@ class AppConfigProvider with ChangeNotifier { return _iosDeviceInfo; } + ThemeMode get selectedTheme { + switch (_selectedTheme) { + case 0: + return SchedulerBinding.instance.window.platformBrightness == Brightness.light + ? ThemeMode.light + : ThemeMode.dark; + + case 1: + return ThemeMode.light; + + case 2: + return ThemeMode.dark; + + default: + return ThemeMode.light; + } + } + + int get selectedThemeNumber { + return _selectedTheme; + } + + void setDbInstance(Database db) { _dbInstance = db; } @@ -37,4 +63,36 @@ class AppConfigProvider with ChangeNotifier { void setIosInfo(IosDeviceInfo deviceInfo) { _iosDeviceInfo = deviceInfo; } + + Future setSelectedTheme(int value) async { + final updated = await _updateThemeDb(value); + if (updated == true) { + _selectedTheme = value; + notifyListeners(); + return true; + } + else { + return false; + } + } + + Future _updateThemeDb(int value) async { + try { + return await _dbInstance!.transaction((txn) async { + await txn.rawUpdate( + 'UPDATE appConfig SET theme = $value', + ); + return true; + }); + } catch (e) { + return false; + } + } + + void saveFromDb(Database dbInstance, Map dbData) { + _selectedTheme = dbData['theme']; + + _dbInstance = dbInstance; + notifyListeners(); + } } \ No newline at end of file diff --git a/lib/screens/settings/appbar.dart b/lib/screens/settings/appbar.dart index 5d91161..7d53c37 100644 --- a/lib/screens/settings/appbar.dart +++ b/lib/screens/settings/appbar.dart @@ -1,5 +1,4 @@ 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); @@ -7,7 +6,24 @@ class SettingsAppBar extends StatelessWidget with PreferredSizeWidget { @override Widget build(BuildContext context) { return AppBar( - title: Text(AppLocalizations.of(context)!.settings), + toolbarHeight: 50, + centerTitle: true, + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/icon/icon1024-white-center.png', + width: 60, + ), + const SizedBox(width: 20), + const Text( + "AdGuard Home Manager", + style: TextStyle( + fontSize: 20 + ), + ) + ], + ), ); } diff --git a/lib/screens/settings/custom_list_tile.dart b/lib/screens/settings/custom_list_tile.dart new file mode 100644 index 0000000..f97cf67 --- /dev/null +++ b/lib/screens/settings/custom_list_tile.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; + +class CustomListTile extends StatelessWidget { + final IconData? leadingIcon; + final String label; + final String? description; + final Color? color; + final void Function()? onTap; + final Widget? trailing; + final EdgeInsets? padding; + + const CustomListTile({ + Key? key, + this.leadingIcon, + required this.label, + this.description, + this.color, + this.onTap, + this.trailing, + this.padding + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + child: Container( + padding: padding ?? const EdgeInsets.symmetric( + vertical: 10, + horizontal: 25 + ), + width: double.maxFinite, + child: Row( + children: [ + if (leadingIcon != null) Row( + children: [ + Icon( + leadingIcon, + color: color, + ), + const SizedBox(width: 20), + ], + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 16, + color: color + ), + ), + if (description != null) Column( + children: [ + const SizedBox(height: 5), + Text( + description!, + style: TextStyle( + color: color ?? Colors.grey + ), + ) + ], + ) + ], + ), + ), + if (trailing != null) trailing! + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/custom_radio.dart b/lib/screens/settings/custom_radio.dart new file mode 100644 index 0000000..868f407 --- /dev/null +++ b/lib/screens/settings/custom_radio.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +class CustomRadio extends StatelessWidget { + final int value; + final int groupValue; + final Function(int)? onChange; + final Color backgroundColor; + + const CustomRadio({ + Key? key, + required this.value, + required this.groupValue, + this.onChange, + required this.backgroundColor, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.center, + children: [ + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(40), + color: value == groupValue + ? Theme.of(context).primaryColor + : Theme.of(context).brightness == Brightness.dark + ? const Color.fromRGBO(184, 184, 184, 1) + : const Color.fromRGBO(104, 104, 104, 1) + ), + ), + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(35), + color: backgroundColor + ), + ), + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: value == groupValue + ? Theme.of(context).primaryColor + : backgroundColor + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/section_label.dart b/lib/screens/settings/section_label.dart new file mode 100644 index 0000000..ea31a1a --- /dev/null +++ b/lib/screens/settings/section_label.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class SectionLabel extends StatelessWidget { + final String label; + + const SectionLabel({ + Key? key, + required this.label + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(25), + child: Text( + label, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16 + ), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/settings/settings.dart b/lib/screens/settings/settings.dart index 0168162..17422aa 100644 --- a/lib/screens/settings/settings.dart +++ b/lib/screens/settings/settings.dart @@ -1,10 +1,60 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/settings/theme_modal.dart'; +import 'package:adguard_home_manager/screens/settings/custom_list_tile.dart'; +import 'package:adguard_home_manager/screens/settings/section_label.dart'; + +import 'package:adguard_home_manager/providers/app_config_provider.dart'; class Settings extends StatelessWidget { const Settings({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return Container(); + final appConfigProvider = Provider.of(context); + + final statusBarHeight = MediaQuery.of(context).viewInsets.top; + + String getThemeString() { + switch (appConfigProvider.selectedThemeNumber) { + case 0: + return AppLocalizations.of(context)!.systemDefined; + + case 1: + return AppLocalizations.of(context)!.light; + + case 2: + return AppLocalizations.of(context)!.dark; + + default: + return ""; + } + } + + void openThemeModal() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => ThemeModal( + statusBarHeight: statusBarHeight, + selectedTheme: appConfigProvider.selectedThemeNumber, + ), + backgroundColor: Colors.transparent, + ); + } + + return ListView( + children: [ + SectionLabel(label: AppLocalizations.of(context)!.appSettings), + CustomListTile( + leadingIcon: Icons.light_mode_rounded, + label: AppLocalizations.of(context)!.theme, + description: getThemeString(), + onTap: openThemeModal, + ), + ], + ); } } \ No newline at end of file diff --git a/lib/screens/settings/theme_modal.dart b/lib/screens/settings/theme_modal.dart new file mode 100644 index 0000000..b984eb1 --- /dev/null +++ b/lib/screens/settings/theme_modal.dart @@ -0,0 +1,180 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/settings/custom_radio.dart'; + +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + + +class ThemeModal extends StatefulWidget { + final double statusBarHeight; + final int selectedTheme; + + const ThemeModal({ + Key? key, + required this.statusBarHeight, + required this.selectedTheme, + }) : super(key: key); + + @override + State createState() => _ThemeModalState(); +} + +class _ThemeModalState extends State { + int _selectedItem = 0; + + @override + void initState() { + _selectedItem = widget.selectedTheme; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final appConfigProvider = Provider.of(context); + + final mediaQuery = MediaQuery.of(context); + + return Container( + height: mediaQuery.orientation == Orientation.landscape + ? mediaQuery.size.height - (widget.statusBarHeight) + : Platform.isIOS ? 408 : 388, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30) + ), + color: Theme.of(context).dialogBackgroundColor, + ), + child: Column( + children: [ + SizedBox( + height: 300, + child: Column( + children: [ + const Padding( + padding: EdgeInsets.only(top: 24), + child: Icon( + Icons.light_mode_rounded, + size: 26, + ), + ), + Padding( + padding: const EdgeInsets.only( + top: 24, + bottom: 24 + ), + child: Text( + AppLocalizations.of(context)!.theme, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 24 + ), + ), + ), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + setState(() => _selectedItem = 0); + appConfigProvider.setSelectedTheme(0); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: ListTile( + leading: const Icon(Icons.phone_android_rounded), + title: Text( + AppLocalizations.of(context)!.systemDefined, + style: const TextStyle( + fontWeight: FontWeight.normal + ), + ), + trailing: CustomRadio( + value: 0, + groupValue: _selectedItem, + backgroundColor: Theme.of(context).dialogBackgroundColor, + ), + ), + ), + ), + ), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + setState(() => _selectedItem = 1); + appConfigProvider.setSelectedTheme(1); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: ListTile( + leading: const Icon(Icons.light_mode_rounded), + title: Text( + AppLocalizations.of(context)!.light, + style: const TextStyle( + fontWeight: FontWeight.normal + ), + ), + trailing: CustomRadio( + value: 1, + groupValue: _selectedItem, + backgroundColor: Theme.of(context).dialogBackgroundColor, + ), + ), + ), + ), + ), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + setState(() => _selectedItem = 2); + appConfigProvider.setSelectedTheme(2); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: ListTile( + leading: const Icon(Icons.dark_mode_rounded), + title: Text( + AppLocalizations.of(context)!.dark, + style: const TextStyle( + fontWeight: FontWeight.normal + ), + ), + trailing: CustomRadio( + value: 2, + groupValue: _selectedItem, + backgroundColor: Theme.of(context).dialogBackgroundColor, + ), + ), + ), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(20), + child: Padding( + padding: EdgeInsets.only( + bottom: Platform.isIOS ? 20 : 0 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.close) + ) + ], + ), + ), + ) + ], + ) + ); + } +} \ No newline at end of file diff --git a/lib/services/database.dart b/lib/services/database.dart index 3085c75..49b84b1 100644 --- a/lib/services/database.dart +++ b/lib/services/database.dart @@ -2,13 +2,15 @@ import 'package:sqflite/sqflite.dart'; Future> loadDb() async { List>? servers; - // List>? appConfig; + 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 TEXT PRIMARY KEY, name TEXT, connectionMethod TEXT, domain TEXT, path TEXT, port INTEGER, user TEXT, password TEXT, defaultServer INTEGER, authToken TEXT)"); + await db.execute("CREATE TABLE appConfig (theme NUMERIC)"); + await db.execute("INSERT INTO appConfig (theme) VALUES (0)"); }, onUpgrade: (Database db, int oldVersion, int newVersion) async { @@ -19,17 +21,17 @@ Future> loadDb() async { 'SELECT * FROM servers', ); }); - // await db.transaction((txn) async{ - // appConfig = await txn.rawQuery( - // 'SELECT * FROM appConfig', - // ); - // }); + await db.transaction((txn) async{ + appConfig = await txn.rawQuery( + 'SELECT * FROM appConfig', + ); + }); } ); return { "servers": servers, - // "appConfig": appConfig![0], + "appConfig": appConfig![0], "dbInstance": db, }; } \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 915cb45..9e61ebc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -92,6 +92,7 @@ flutter: - assets/icon/icon-android-adap.png - assets/icon/icon-android.png - assets/icon/icon-splash.png + - assets/icon/icon1024-white-center.png # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware