Merge branch 'beta'

This commit is contained in:
Juan Gilsanz Polo 2023-10-08 21:46:50 +02:00
commit 4f0ba59713
50 changed files with 2417 additions and 2339 deletions

View file

@ -4,6 +4,7 @@
"connect": "Connect", "connect": "Connect",
"servers": "Servers", "servers": "Servers",
"createConnection": "Create connection", "createConnection": "Create connection",
"editConnection": "Edit connection",
"name": "Name", "name": "Name",
"ipDomain": "IP address or domain", "ipDomain": "IP address or domain",
"path": "Path", "path": "Path",
@ -316,6 +317,7 @@
"deletingRule": "Deleting rule...", "deletingRule": "Deleting rule...",
"enablingList": "Enabling list...", "enablingList": "Enabling list...",
"disablingList": "Disabling list...", "disablingList": "Disabling list...",
"savingList": "Saving list...",
"disableFiltering": "Disable filtering", "disableFiltering": "Disable filtering",
"enablingFiltering": "Enabling filtering...", "enablingFiltering": "Enabling filtering...",
"disablingFiltering": "Disabling filtering...", "disablingFiltering": "Disabling filtering...",

View file

@ -4,6 +4,7 @@
"connect": "Conectar", "connect": "Conectar",
"servers": "Servidores", "servers": "Servidores",
"createConnection": "Crear conexión", "createConnection": "Crear conexión",
"editConnection": "Editar conexión",
"name": "Nombre", "name": "Nombre",
"ipDomain": "Dirección IP o dominio", "ipDomain": "Dirección IP o dominio",
"path": "Ruta", "path": "Ruta",
@ -316,6 +317,7 @@
"deletingRule": "Eliminando regla...", "deletingRule": "Eliminando regla...",
"enablingList": "Habilitando lista...", "enablingList": "Habilitando lista...",
"disablingList": "Deshabilitando lista...", "disablingList": "Deshabilitando lista...",
"savingList": "Guardando lista...",
"disableFiltering": "Deshabilitar filtrado", "disableFiltering": "Deshabilitar filtrado",
"enablingFiltering": "Habilitando filtrado...", "enablingFiltering": "Habilitando filtrado...",
"disablingFiltering": "Deshabilitando filtrado...", "disablingFiltering": "Deshabilitando filtrado...",

View file

@ -13,7 +13,7 @@ class DnsInfo {
int? cacheTtlMin; int? cacheTtlMin;
int? cacheTtlMax; int? cacheTtlMax;
bool? cacheOptimistic; bool? cacheOptimistic;
bool resolveClients; bool? resolveClients;
bool usePrivatePtrResolvers; bool usePrivatePtrResolvers;
List<String> localPtrUpstreams; List<String> localPtrUpstreams;
String blockingIpv4; String blockingIpv4;

View file

@ -325,14 +325,14 @@ class FilteringProvider with ChangeNotifier {
}; };
} }
} }
else if (result1['result'] == 'error' && result1['log'].statusCode == '400' && result1['log'].resBody.toString().contains("Couldn't fetch filter from url")) { else if (result1['result'] == 'error' && result1['log'].statusCode == '400' && result1['log'].resBody.toString().contains("data is HTML, not plain text")) {
notifyListeners(); notifyListeners();
return { return {
'success': false, 'success': false,
'error': 'invalid_url' 'error': 'invalid_url'
}; };
} }
else if (result1['result'] == 'error' && result1['log'].statusCode == '400' && result1['log'].resBody.toString().contains('Filter URL already added')) { else if (result1['result'] == 'error' && result1['log'].statusCode == '400' && result1['log'].resBody.toString().contains('url already exists')) {
notifyListeners(); notifyListeners();
return { return {
'success': false, 'success': false,

View file

@ -9,9 +9,9 @@ import 'package:flutter_split_view/flutter_split_view.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/clients/client_screen.dart'; import 'package:adguard_home_manager/screens/clients/client/client_screen_functions.dart';
import 'package:adguard_home_manager/screens/clients/added_client_tile.dart'; import 'package:adguard_home_manager/screens/clients/client/added_client_tile.dart';
import 'package:adguard_home_manager/screens/clients/remove_client_modal.dart'; import 'package:adguard_home_manager/screens/clients/client/remove_client_modal.dart';
import 'package:adguard_home_manager/screens/clients/fab.dart'; import 'package:adguard_home_manager/screens/clients/fab.dart';
import 'package:adguard_home_manager/screens/clients/options_modal.dart'; import 'package:adguard_home_manager/screens/clients/options_modal.dart';
import 'package:adguard_home_manager/widgets/tab_content_list.dart'; import 'package:adguard_home_manager/widgets/tab_content_list.dart';
@ -128,32 +128,13 @@ class _AddedListState extends State<AddedList> {
} }
void openClientModal(Client client) { void openClientModal(Client client) {
if (width > 900 || !(Platform.isAndroid | Platform.isIOS)) { openClientFormModal(
showDialog(
barrierDismissible: false,
context: context, context: context,
builder: (BuildContext context) => ClientScreen( width: width,
onConfirm: confirmEditClient, onConfirm: confirmEditClient,
serverVersion: statusProvider.serverStatus!.serverVersion, onDelete: deleteClient
onDelete: deleteClient,
client: client,
dialog: true,
)
); );
} }
else {
Navigator.push(context, MaterialPageRoute(
fullscreenDialog: true,
builder: (BuildContext context) => ClientScreen(
onConfirm: confirmEditClient,
serverVersion: statusProvider.serverStatus!.serverVersion,
onDelete: deleteClient,
client: client,
dialog: false,
)
));
}
}
void openDeleteModal(Client client) { void openDeleteModal(Client client) {
showModal( showModal(
@ -204,7 +185,10 @@ class _AddedListState extends State<AddedList> {
client: widget.data[index], client: widget.data[index],
onTap: widget.onClientSelected, onTap: widget.onClientSelected,
onLongPress: openOptionsModal, onLongPress: openOptionsModal,
onEdit: openClientModal, onEdit: statusProvider.serverStatus != null
? (c) => openClientModal(c)
: null,
onDelete: openDeleteModal,
splitView: widget.splitView, splitView: widget.splitView,
serverVersion: statusProvider.serverStatus!.serverVersion, serverVersion: statusProvider.serverStatus!.serverVersion,
), ),

View file

@ -14,7 +14,8 @@ class AddedClientTile extends StatelessWidget {
final Client client; final Client client;
final void Function(Client) onTap; final void Function(Client) onTap;
final void Function(Client) onLongPress; final void Function(Client) onLongPress;
final void Function(Client) onEdit; final void Function(Client)? onEdit;
final void Function(Client) onDelete;
final Client? selectedClient; final Client? selectedClient;
final bool? splitView; final bool? splitView;
final String serverVersion; final String serverVersion;
@ -24,7 +25,8 @@ class AddedClientTile extends StatelessWidget {
required this.client, required this.client,
required this.onTap, required this.onTap,
required this.onLongPress, required this.onLongPress,
required this.onEdit, this.onEdit,
required this.onDelete,
this.selectedClient, this.selectedClient,
required this.splitView, required this.splitView,
required this.serverVersion required this.serverVersion
@ -42,12 +44,20 @@ class AddedClientTile extends StatelessWidget {
borderRadius: BorderRadius.circular(28), borderRadius: BorderRadius.circular(28),
child: ContextMenuArea( child: ContextMenuArea(
builder: (context) => [ builder: (context) => [
CustomListTile( if (onEdit != null) CustomListTile(
title: AppLocalizations.of(context)!.seeDetails, title: AppLocalizations.of(context)!.edit,
icon: Icons.file_open_rounded, icon: Icons.edit_rounded,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
onEdit(client); onEdit!(client);
}
),
CustomListTile(
title: AppLocalizations.of(context)!.delete,
icon: Icons.delete_rounded,
onTap: () {
Navigator.pop(context);
onDelete(client);
} }
), ),
CustomListTile( CustomListTile(
@ -175,12 +185,12 @@ class AddedClientTile extends StatelessWidget {
else { else {
return ContextMenuArea( return ContextMenuArea(
builder: (context) => [ builder: (context) => [
CustomListTile( if (onEdit != null) CustomListTile(
title: AppLocalizations.of(context)!.seeDetails, title: AppLocalizations.of(context)!.seeDetails,
icon: Icons.file_open_rounded, icon: Icons.file_open_rounded,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
onEdit(client); onEdit!(client);
} }
), ),
CustomListTile( CustomListTile(

View file

@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/clients/client/client_screen_functions.dart';
class BlockedServicesSection extends StatelessWidget {
final bool useGlobalSettingsServices;
final List<String> blockedServices;
final void Function(List<String>) onUpdatedBlockedServices;
final void Function(bool) onUpdateServicesGlobalSettings;
const BlockedServicesSection({
Key? key,
required this.useGlobalSettingsServices,
required this.blockedServices,
required this.onUpdatedBlockedServices,
required this.onUpdateServicesGlobalSettings
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Material(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(28),
child: InkWell(
onTap: () => onUpdateServicesGlobalSettings(!useGlobalSettingsServices),
borderRadius: BorderRadius.circular(28),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 5
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppLocalizations.of(context)!.useGlobalSettings,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface
),
),
Switch(
value: useGlobalSettingsServices,
onChanged: (value) => onUpdateServicesGlobalSettings(value),
)
],
),
),
),
),
),
const SizedBox(height: 10),
Material(
color: Colors.transparent,
child: InkWell(
onTap: useGlobalSettingsServices == false
? () => openServicesModal(
context: context,
blockedServices: blockedServices,
onUpdateBlockedServices: onUpdatedBlockedServices
)
: null,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8, horizontal: 24
),
child: Row(
children: [
Icon(
Icons.public,
color: useGlobalSettingsServices == false
? Theme.of(context).listTileTheme.iconColor
: Theme.of(context).colorScheme.onSurface.withOpacity(0.38),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.selectBlockedServices,
style: TextStyle(
fontSize: 16,
color: useGlobalSettingsServices == false
? Theme.of(context).colorScheme.onSurface
: Theme.of(context).colorScheme.onSurface.withOpacity(0.38),
),
),
if (useGlobalSettingsServices == false) ...[
const SizedBox(height: 5),
Text(
blockedServices.isNotEmpty
? "${blockedServices.length} ${AppLocalizations.of(context)!.servicesBlocked}"
: AppLocalizations.of(context)!.noBlockedServicesSelected,
style: TextStyle(
color: Theme.of(context).listTileTheme.iconColor
),
)
]
],
)
],
),
),
),
),
],
);
}
}

View file

@ -0,0 +1,442 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/clients/client/upstream_servers_section.dart';
import 'package:adguard_home_manager/screens/clients/client/identifiers_section.dart';
import 'package:adguard_home_manager/screens/clients/client/blocked_services_section.dart';
import 'package:adguard_home_manager/screens/clients/client/tags_section.dart';
import 'package:adguard_home_manager/screens/clients/client/settings_tile.dart';
import 'package:adguard_home_manager/screens/clients/client/client_screen_functions.dart';
import 'package:adguard_home_manager/widgets/section_label.dart';
import 'package:adguard_home_manager/widgets/custom_list_tile.dart';
import 'package:adguard_home_manager/functions/compare_versions.dart';
import 'package:adguard_home_manager/models/safe_search.dart';
import 'package:adguard_home_manager/providers/clients_provider.dart';
import 'package:adguard_home_manager/providers/status_provider.dart';
import 'package:adguard_home_manager/models/clients.dart';
class ClientScreen extends StatefulWidget {
final Client? client;
final void Function(Client) onConfirm;
final void Function(Client)? onDelete;
final bool fullScreen;
const ClientScreen({
Key? key,
this.client,
required this.onConfirm,
this.onDelete,
required this.fullScreen
}) : super(key: key);
@override
State<ClientScreen> createState() => _ClientScreenState();
}
class _ClientScreenState extends State<ClientScreen> {
final Uuid uuid = const Uuid();
bool validValues = false;
TextEditingController nameController = TextEditingController();
List<String> selectedTags = [];
List<Map<dynamic, dynamic>> identifiersControllers = [
{
'id': 0,
'controller': TextEditingController()
}
];
bool useGlobalSettingsFiltering = true;
bool? enableFiltering;
bool? enableSafeBrowsing;
bool? enableParentalControl;
bool? enableSafeSearch;
SafeSearch? safeSearch;
final SafeSearch defaultSafeSearch = SafeSearch(
enabled: false,
bing: false,
duckduckgo: false,
google: false,
pixabay: false,
yandex: false,
youtube: false
);
bool useGlobalSettingsServices = true;
List<String> blockedServices = [];
List<Map<dynamic, dynamic>> upstreamServers = [];
bool version = false;
@override
void initState() {
version = serverVersionIsAhead(
currentVersion: Provider.of<StatusProvider>(context, listen: false).serverStatus!.serverVersion,
referenceVersion: 'v0.107.28',
referenceVersionBeta: 'v0.108.0-b.33'
);
if (widget.client != null) {
validValues = true;
nameController.text = widget.client!.name;
selectedTags = widget.client!.tags;
identifiersControllers = widget.client!.ids.map((e) => {
'id': uuid.v4(),
'controller': TextEditingController(text: e)
}).toList();
useGlobalSettingsFiltering = widget.client!.useGlobalSettings;
enableFiltering = widget.client!.filteringEnabled;
enableParentalControl = widget.client!.parentalEnabled;
enableSafeBrowsing = widget.client!.safebrowsingEnabled;
if (version == true) {
safeSearch = widget.client!.safeSearch;
}
else {
enableSafeSearch = widget.client!.safesearchEnabled ?? false;
}
useGlobalSettingsServices = widget.client!.useGlobalBlockedServices;
blockedServices = widget.client!.blockedServices;
upstreamServers = widget.client!.upstreams.map((e) => {
'id': uuid.v4(),
'controller': TextEditingController(text: e)
}).toList();
}
super.initState();
}
@override
Widget build(BuildContext context) {
final clientsProvider = Provider.of<ClientsProvider>(context);
final statusProvider = Provider.of<StatusProvider>(context);
void createClient() {
final Client client = Client(
name: nameController.text,
ids: List<String>.from(identifiersControllers.map((e) => e['controller'].text)),
useGlobalSettings: useGlobalSettingsFiltering,
filteringEnabled: enableFiltering ?? false,
parentalEnabled: enableParentalControl ?? false,
safebrowsingEnabled: enableSafeBrowsing ?? false,
safesearchEnabled: version == false ? enableSafeSearch : null,
safeSearch: version == true ? safeSearch : null,
useGlobalBlockedServices: useGlobalSettingsServices,
blockedServices: blockedServices,
upstreams: List<String>.from(upstreamServers.map((e) => e['controller'].text)),
tags: selectedTags
);
widget.onConfirm(client);
}
void enableDisableGlobalSettingsFiltering() {
if (useGlobalSettingsFiltering == true) {
setState(() {
useGlobalSettingsFiltering = false;
enableFiltering = false;
enableSafeBrowsing = false;
enableParentalControl = false;
enableSafeSearch = false;
safeSearch = defaultSafeSearch;
});
}
else if (useGlobalSettingsFiltering == false) {
setState(() {
useGlobalSettingsFiltering = true;
enableFiltering = null;
enableSafeBrowsing = null;
enableParentalControl = null;
enableSafeSearch = null;
safeSearch = null;
});
}
}
void updateServicesGlobalSettings(bool value) {
if (value == true) {
setState(() {
blockedServices = [];
useGlobalSettingsServices = true;
});
}
else if (value == false) {
setState(() {
useGlobalSettingsServices = false;
});
}
}
List<Widget> actions() {
return [
IconButton(
onPressed: validValues == true
? () {
createClient();
Navigator.pop(context);
}
: null,
icon: const Icon(Icons.save_rounded),
tooltip: AppLocalizations.of(context)!.save,
),
if (widget.client != null) IconButton(
onPressed: () => openDeleteClientScreen(
context: context,
onDelete: () => clientsProvider.deleteClient(widget.client!),
),
icon: const Icon(Icons.delete_rounded),
tooltip: AppLocalizations.of(context)!.delete,
),
const SizedBox(width: 10),
];
}
Widget content(bool withPaddingTop) {
return ListView(
padding: const EdgeInsets.only(top: 0),
children: [
if (withPaddingTop == true) const SizedBox(height: 24),
if (withPaddingTop == false) const SizedBox(height: 6),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: TextFormField(
enabled: widget.client != null ? false : true,
controller: nameController,
onChanged: (_) => setState(() {
validValues = checkValidValues(
identifiersControllers: identifiersControllers,
nameController: nameController
);
}),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.badge_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
labelText: AppLocalizations.of(context)!.name,
),
),
),
SectionLabel(
label: AppLocalizations.of(context)!.tags,
padding: const EdgeInsets.all(24),
),
TagsSection(
selectedTags: selectedTags,
onTagsSelected: (tags) => setState(() => selectedTags = tags)
),
IdentifiersSection(
identifiersControllers: identifiersControllers,
onUpdateIdentifiersControllers: (c) => setState(() {
identifiersControllers = c;
validValues = checkValidValues(
nameController: nameController,
identifiersControllers: identifiersControllers
);
}),
onCheckValidValues: () => setState(() {
validValues = checkValidValues(
identifiersControllers: identifiersControllers,
nameController: nameController
);
}),
),
SectionLabel(
label: AppLocalizations.of(context)!.settings,
padding: const EdgeInsets.only(
left: 24, right: 24, top: 12, bottom: 24
)
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Material(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(28),
child: InkWell(
onTap: () => enableDisableGlobalSettingsFiltering(),
borderRadius: BorderRadius.circular(28),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 5
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppLocalizations.of(context)!.useGlobalSettings,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface
),
),
Switch(
value: useGlobalSettingsFiltering,
onChanged: (value) => enableDisableGlobalSettingsFiltering()
)
],
),
),
),
),
),
const SizedBox(height: 10),
SettingsTile(
label: AppLocalizations.of(context)!.enableFiltering,
value: enableFiltering,
onChange: (value) => setState(() => enableFiltering = value),
useGlobalSettingsFiltering: useGlobalSettingsFiltering,
),
SettingsTile(
label: AppLocalizations.of(context)!.enableSafeBrowsing,
value: enableSafeBrowsing,
onChange: (value) => setState(() => enableSafeBrowsing = value),
useGlobalSettingsFiltering: useGlobalSettingsFiltering,
),
SettingsTile(
label: AppLocalizations.of(context)!.enableParentalControl,
value: enableParentalControl,
onChange: (value) => setState(() => enableParentalControl = value),
useGlobalSettingsFiltering: useGlobalSettingsFiltering,
),
if (
serverVersionIsAhead(
currentVersion: statusProvider.serverStatus!.serverVersion,
referenceVersion: 'v0.107.28',
referenceVersionBeta: 'v0.108.0-b.33'
) == true
) CustomListTile(
title: AppLocalizations.of(context)!.safeSearch,
padding: const EdgeInsets.symmetric(
horizontal: 42,
vertical: 16
),
trailing: Padding(
padding: const EdgeInsets.only(right: 16),
child: Icon(
Icons.chevron_right_rounded,
color: useGlobalSettingsFiltering == true
? Colors.grey
: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
onTap: useGlobalSettingsFiltering == false
? () => openSafeSearchModal(
context: context,
blockedServices: blockedServices,
defaultSafeSearch: defaultSafeSearch,
safeSearch: safeSearch,
onUpdateSafeSearch: (s) => setState(() => safeSearch = s)
)
: null,
),
if (
serverVersionIsAhead(
currentVersion: statusProvider.serverStatus!.serverVersion,
referenceVersion: 'v0.107.28',
referenceVersionBeta: 'v0.108.0-b.33'
) == false
) SettingsTile(
label: AppLocalizations.of(context)!.enableSafeSearch,
value: enableSafeSearch,
onChange: (value) => setState(() => enableSafeSearch = value),
useGlobalSettingsFiltering: useGlobalSettingsFiltering,
),
SectionLabel(
label: AppLocalizations.of(context)!.blockedServices,
padding: const EdgeInsets.all(24),
),
BlockedServicesSection(
useGlobalSettingsServices: useGlobalSettingsServices,
blockedServices: blockedServices,
onUpdatedBlockedServices: (s) => setState(() => blockedServices = s),
onUpdateServicesGlobalSettings: (v) => setState(() => useGlobalSettingsServices = v),
),
UpstreamServersSection(
upstreamServers: upstreamServers,
onCheckValidValues: () => setState(() {
validValues = checkValidValues(
identifiersControllers: identifiersControllers,
nameController: nameController
);
}),
onUpdateUpstreamServers: (v) => setState(() => upstreamServers = v)
),
const SizedBox(height: 20)
],
);
}
if (widget.fullScreen == true) {
return Dialog.fullscreen(
child: Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close)
),
title: Text(
widget.client != null
? AppLocalizations.of(context)!.client
: AppLocalizations.of(context)!.addClient
),
actions: actions(),
),
body: content(true)
),
);
}
else {
return Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 500
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
CloseButton(onPressed: () => Navigator.pop(context)),
const SizedBox(width: 8),
Text(
widget.client != null
? AppLocalizations.of(context)!.client
: AppLocalizations.of(context)!.addClient,
style: const TextStyle(
fontSize: 22
),
),
],
),
Row(
children: actions()
)
],
),
),
Flexible(
child: content(false)
)
],
),
),
);
}
}
}

View file

@ -0,0 +1,126 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:adguard_home_manager/screens/clients/client/client_screen.dart';
import 'package:adguard_home_manager/screens/clients/client/remove_client_modal.dart';
import 'package:adguard_home_manager/screens/clients/client/safe_search_modal.dart';
import 'package:adguard_home_manager/screens/clients/client/services_modal.dart';
import 'package:adguard_home_manager/screens/clients/client/tags_modal.dart';
import 'package:adguard_home_manager/models/clients.dart';
import 'package:adguard_home_manager/providers/clients_provider.dart';
import 'package:adguard_home_manager/models/safe_search.dart';
void openTagsModal({
required BuildContext context,
required List<String> selectedTags,
required void Function(List<String>) onSelectedTags
}) {
showDialog(
context: context,
builder: (context) => TagsModal(
selectedTags: selectedTags,
tags: Provider.of<ClientsProvider>(context, listen: false).clients!.supportedTags,
onConfirm: onSelectedTags,
)
);
}
void openServicesModal({
required BuildContext context,
required List<String> blockedServices,
required void Function(List<String>) onUpdateBlockedServices
}) {
showDialog(
context: context,
builder: (context) => ServicesModal(
blockedServices: blockedServices,
onConfirm: onUpdateBlockedServices,
)
);
}
void openDeleteClientScreen({
required BuildContext context,
required void Function() onDelete
}) {
showDialog(
context: context,
builder: (ctx) => RemoveClientModal(
onConfirm: () {
Navigator.pop(context);
onDelete();
}
)
);
}
void openSafeSearchModal({
required BuildContext context,
required List<String> blockedServices,
required void Function(SafeSearch) onUpdateSafeSearch,
required SafeSearch? safeSearch,
required SafeSearch defaultSafeSearch
}) {
showDialog(
context: context,
builder: (context) => SafeSearchModal(
safeSearch: safeSearch ?? defaultSafeSearch,
disabled: false,
onConfirm: onUpdateSafeSearch
)
);
}
bool checkValidValues({
required TextEditingController nameController,
required List<Map<dynamic, dynamic>> identifiersControllers
}) {
if (
nameController.text != '' &&
identifiersControllers.isNotEmpty &&
identifiersControllers[0]['controller']!.text != ''
) {
return true;
}
else {
return false;
}
}
void openClientFormModal({
required BuildContext context,
required double width,
Client? client,
required void Function(Client) onConfirm,
void Function(Client)? onDelete,
}) {
showGeneralDialog(
context: context,
barrierColor: !(width > 900 || !(Platform.isAndroid | Platform.isIOS))
?Colors.transparent
: Colors.black54,
transitionBuilder: (context, anim1, anim2, child) {
return SlideTransition(
position: Tween(
begin: const Offset(0, 1),
end: const Offset(0, 0)
).animate(
CurvedAnimation(
parent: anim1,
curve: Curves.easeInOutCubicEmphasized
)
),
child: child,
);
},
pageBuilder: (context, animation, secondaryAnimation) => ClientScreen(
fullScreen: !(width > 900 || !(Platform.isAndroid | Platform.isIOS)),
client: client,
onConfirm: onConfirm,
onDelete: onDelete,
),
);
}

View file

@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/widgets/section_label.dart';
class IdentifiersSection extends StatefulWidget {
final List<Map<dynamic, dynamic>> identifiersControllers;
final void Function(List<Map<dynamic, dynamic>>) onUpdateIdentifiersControllers;
final void Function() onCheckValidValues;
const IdentifiersSection({
Key? key,
required this.identifiersControllers,
required this.onUpdateIdentifiersControllers,
required this.onCheckValidValues
}) : super(key: key);
@override
State<IdentifiersSection> createState() => _IdentifiersSectionState();
}
class _IdentifiersSectionState extends State<IdentifiersSection> {
final Uuid uuid = const Uuid();
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SectionLabel(
label: AppLocalizations.of(context)!.identifiers,
padding: const EdgeInsets.only(
left: 24, right: 24, top: 24, bottom: 12
)
),
Padding(
padding: const EdgeInsets.only(right: 20),
child: IconButton(
onPressed: () => widget.onUpdateIdentifiersControllers([
...widget.identifiersControllers,
Map<String, Object>.from({
'id': uuid.v4(),
'controller': TextEditingController()
})
]),
icon: const Icon(Icons.add)
),
)
],
),
if (widget.identifiersControllers.isNotEmpty) ...widget.identifiersControllers.map((controller) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextFormField(
controller: controller['controller'],
onChanged: (_) => widget.onCheckValidValues(),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.tag),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
helperText: AppLocalizations.of(context)!.identifierHelper,
labelText: AppLocalizations.of(context)!.identifier,
),
),
),
const SizedBox(width: 20),
Padding(
padding: const EdgeInsets.only(bottom: 25),
child: IconButton(
onPressed: () => widget.onUpdateIdentifiersControllers(
widget.identifiersControllers.where((e) => e['id'] != controller['id']).toList()
),
icon: const Icon(Icons.remove_circle_outline_outlined)
),
)
],
),
)).toList(),
if (widget.identifiersControllers.isEmpty) Container(
padding: const EdgeInsets.only(top: 10),
child: Text(
AppLocalizations.of(context)!.noIdentifiers,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
);
}
}

View file

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
class SettingsTile extends StatelessWidget {
final String label;
final bool? value;
final void Function(bool)? onChange;
final bool useGlobalSettingsFiltering;
const SettingsTile({
Key? key,
required this.label,
required this.value,
this.onChange,
required this.useGlobalSettingsFiltering
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onChange != null
? value != null ? () => onChange!(!value!) : null
: null,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 42,
vertical: 5
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface,
),
),
useGlobalSettingsFiltering == false
? Switch(
value: value!,
onChanged: onChange,
)
: Padding(
padding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 12
),
child: Text(
"Global",
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
)
],
),
),
),
);
}
}

View file

@ -0,0 +1,63 @@
import 'package:adguard_home_manager/screens/clients/client/client_screen_functions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class TagsSection extends StatelessWidget {
final List<String> selectedTags;
final void Function(List<String>) onTagsSelected;
const TagsSection({
Key? key,
required this.selectedTags,
required this.onTagsSelected
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () => openTagsModal(
context: context,
selectedTags: selectedTags,
onSelectedTags: onTagsSelected
) ,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 0, horizontal: 24
),
child: Row(
children: [
Icon(
Icons.label_rounded,
color: Theme.of(context).listTileTheme.iconColor
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.selectTags,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface
),
),
const SizedBox(height: 3),
Text(
selectedTags.isNotEmpty
? "${selectedTags.length} ${AppLocalizations.of(context)!.tagsSelected}"
: AppLocalizations.of(context)!.noTagsSelected,
style: TextStyle(
color: Theme.of(context).listTileTheme.iconColor
),
)
],
)
],
),
),
),
);
}
}

View file

@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/widgets/section_label.dart';
class UpstreamServersSection extends StatefulWidget {
final List<Map<dynamic, dynamic>> upstreamServers;
final void Function() onCheckValidValues;
final void Function(List<Map<dynamic, dynamic>>) onUpdateUpstreamServers;
const UpstreamServersSection({
Key? key,
required this.upstreamServers,
required this.onCheckValidValues,
required this.onUpdateUpstreamServers
}) : super(key: key);
@override
State<UpstreamServersSection> createState() => _UpstreamServersSectionState();
}
class _UpstreamServersSectionState extends State<UpstreamServersSection> {
final Uuid uuid = const Uuid();
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SectionLabel(
label: AppLocalizations.of(context)!.upstreamServers,
padding: const EdgeInsets.all(24),
),
Padding(
padding: const EdgeInsets.only(right: 20),
child: IconButton(
onPressed: () => setState(() => widget.upstreamServers.add(
Map<String, Object>.from({
'id': uuid.v4(),
'controller': TextEditingController()
})
)),
icon: const Icon(Icons.add)
),
)
],
),
if (widget.upstreamServers.isNotEmpty) ...widget.upstreamServers.map((controller) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextFormField(
controller: controller['controller'],
onChanged: (_) => widget.onCheckValidValues,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.dns_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
labelText: AppLocalizations.of(context)!.serverAddress,
),
),
),
const SizedBox(width: 20),
IconButton(
onPressed: () => widget.onUpdateUpstreamServers(
widget.upstreamServers.where((e) => e['id'] != controller['id']).toList()
),
icon: const Icon(Icons.remove_circle_outline_outlined)
)
],
),
),
)).toList(),
if (widget.upstreamServers.isEmpty) Container(
padding: const EdgeInsets.only(top: 12),
child: Column(
children: [
Text(
AppLocalizations.of(context)!.noUpstreamServers,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 10),
Text(
AppLocalizations.of(context)!.willBeUsedGeneralServers,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
],
);
}
}

View file

@ -1,838 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/clients/remove_client_modal.dart';
import 'package:adguard_home_manager/screens/clients/safe_search_modal.dart';
import 'package:adguard_home_manager/screens/clients/services_modal.dart';
import 'package:adguard_home_manager/screens/clients/tags_modal.dart';
import 'package:adguard_home_manager/widgets/custom_list_tile.dart';
import 'package:adguard_home_manager/functions/compare_versions.dart';
import 'package:adguard_home_manager/models/safe_search.dart';
import 'package:adguard_home_manager/providers/clients_provider.dart';
import 'package:adguard_home_manager/providers/status_provider.dart';
import 'package:adguard_home_manager/models/clients.dart';
class ClientScreen extends StatefulWidget {
final Client? client;
final String serverVersion;
final void Function(Client) onConfirm;
final void Function(Client)? onDelete;
final bool dialog;
const ClientScreen({
Key? key,
this.client,
required this.serverVersion,
required this.onConfirm,
this.onDelete,
required this.dialog
}) : super(key: key);
@override
State<ClientScreen> createState() => _ClientScreenState();
}
class _ClientScreenState extends State<ClientScreen> {
final Uuid uuid = const Uuid();
bool editMode = true;
bool validValues = false;
TextEditingController nameController = TextEditingController();
List<String> selectedTags = [];
List<Map<dynamic, dynamic>> identifiersControllers = [
{
'id': 0,
'controller': TextEditingController()
}
];
bool useGlobalSettingsFiltering = true;
bool? enableFiltering;
bool? enableSafeBrowsing;
bool? enableParentalControl;
bool? enableSafeSearch;
SafeSearch? safeSearch;
final SafeSearch defaultSafeSearch = SafeSearch(
enabled: false,
bing: false,
duckduckgo: false,
google: false,
pixabay: false,
yandex: false,
youtube: false
);
bool useGlobalSettingsServices = true;
List<String> blockedServices = [];
List<Map<dynamic, dynamic>> upstreamServers = [];
void checkValidValues() {
if (
nameController.text != '' &&
identifiersControllers.isNotEmpty &&
identifiersControllers[0]['controller'].text != ''
) {
setState(() => validValues = true);
}
else {
setState(() => validValues = false);
}
}
bool version = false;
@override
void initState() {
version = serverVersionIsAhead(
currentVersion: widget.serverVersion,
referenceVersion: 'v0.107.28',
referenceVersionBeta: 'v0.108.0-b.33'
);
if (widget.client != null) {
editMode = false;
validValues = true;
nameController.text = widget.client!.name;
selectedTags = widget.client!.tags;
identifiersControllers = widget.client!.ids.map((e) => {
'id': uuid.v4(),
'controller': TextEditingController(text: e)
}).toList();
useGlobalSettingsFiltering = widget.client!.useGlobalSettings;
enableFiltering = widget.client!.filteringEnabled;
enableParentalControl = widget.client!.parentalEnabled;
enableSafeBrowsing = widget.client!.safebrowsingEnabled;
if (version == true) {
safeSearch = widget.client!.safeSearch;
}
else {
enableSafeSearch = widget.client!.safesearchEnabled ?? false;
}
useGlobalSettingsServices = widget.client!.useGlobalBlockedServices;
blockedServices = widget.client!.blockedServices;
upstreamServers = widget.client!.upstreams.map((e) => {
'id': uuid.v4(),
'controller': TextEditingController(text: e)
}).toList();
}
super.initState();
}
@override
Widget build(BuildContext context) {
final clientsProvider = Provider.of<ClientsProvider>(context);
final statusProvider = Provider.of<StatusProvider>(context);
void createClient() {
final Client client = Client(
name: nameController.text,
ids: List<String>.from(identifiersControllers.map((e) => e['controller'].text)),
useGlobalSettings: useGlobalSettingsFiltering,
filteringEnabled: enableFiltering ?? false,
parentalEnabled: enableParentalControl ?? false,
safebrowsingEnabled: enableSafeBrowsing ?? false,
safesearchEnabled: version == false ? enableSafeSearch : null,
safeSearch: version == true ? safeSearch : null,
useGlobalBlockedServices: useGlobalSettingsServices,
blockedServices: blockedServices,
upstreams: List<String>.from(upstreamServers.map((e) => e['controller'].text)),
tags: selectedTags
);
widget.onConfirm(client);
}
Widget sectionLabel({
required String label,
EdgeInsets? padding
}) {
return Padding(
padding: padding ?? const EdgeInsets.symmetric(
vertical: 24,
horizontal: 24
),
child: Text(
label,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16,
color: Theme.of(context).colorScheme.primary
),
),
);
}
void enableDisableGlobalSettingsFiltering() {
if (useGlobalSettingsFiltering == true) {
setState(() {
useGlobalSettingsFiltering = false;
enableFiltering = false;
enableSafeBrowsing = false;
enableParentalControl = false;
enableSafeSearch = false;
safeSearch = defaultSafeSearch;
});
}
else if (useGlobalSettingsFiltering == false) {
setState(() {
useGlobalSettingsFiltering = true;
enableFiltering = null;
enableSafeBrowsing = null;
enableParentalControl = null;
enableSafeSearch = null;
safeSearch = null;
});
}
}
void openTagsModal() {
showDialog(
context: context,
builder: (context) => TagsModal(
selectedTags: selectedTags,
tags: clientsProvider.clients!.supportedTags,
onConfirm: (selected) => setState(() => selectedTags = selected),
)
);
}
void openServicesModal() {
showDialog(
context: context,
builder: (context) => ServicesModal(
blockedServices: blockedServices,
onConfirm: (values) => setState(() => blockedServices = values),
)
);
}
void updateServicesGlobalSettings(bool value) {
if (value == true) {
setState(() {
blockedServices = [];
useGlobalSettingsServices = true;
});
}
else if (value == false) {
setState(() {
useGlobalSettingsServices = false;
});
}
}
void openDeleteClientScreen() {
showDialog(
context: context,
builder: (ctx) => RemoveClientModal(
onConfirm: () {
Navigator.pop(context);
widget.onDelete!(widget.client!);
}
)
);
}
void openSafeSearchModal() {
showDialog(
context: context,
builder: (context) => SafeSearchModal(
safeSearch: safeSearch ?? defaultSafeSearch,
disabled: !editMode,
onConfirm: (s) => setState(() => safeSearch = s)
)
);
}
Widget settignsTile({
required String label,
required bool? value,
void Function(bool)? onChange
}) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onChange != null
? value != null ? () => onChange(!value) : null
: null,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 42,
vertical: 5
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface,
),
),
useGlobalSettingsFiltering == false
? Switch(
value: value!,
onChanged: onChange,
)
: Padding(
padding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 12
),
child: Text(
"Global",
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
)
],
),
),
),
);
}
Widget content(bool withPaddingTop) {
return ListView(
padding: const EdgeInsets.only(top: 0),
children: [
if (withPaddingTop == true) const SizedBox(height: 24),
if (withPaddingTop == false) const SizedBox(height: 6),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: TextFormField(
enabled: widget.client != null ? false : true,
controller: nameController,
onChanged: (_) => checkValidValues(),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.badge_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
labelText: AppLocalizations.of(context)!.name,
),
),
),
sectionLabel(label: AppLocalizations.of(context)!.tags),
Material(
color: Colors.transparent,
child: InkWell(
onTap: editMode == true ? () => openTagsModal() : null,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 0, horizontal: 24
),
child: Row(
children: [
Icon(
Icons.label_rounded,
color: Theme.of(context).listTileTheme.iconColor
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.selectTags,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface
),
),
const SizedBox(height: 3),
Text(
selectedTags.isNotEmpty
? "${selectedTags.length} ${AppLocalizations.of(context)!.tagsSelected}"
: AppLocalizations.of(context)!.noTagsSelected,
style: TextStyle(
color: Theme.of(context).listTileTheme.iconColor
),
)
],
)
],
),
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
sectionLabel(
label: AppLocalizations.of(context)!.identifiers,
padding: const EdgeInsets.only(
left: 24, right: 24, top: 24, bottom: 12
)
),
if (editMode == true) Padding(
padding: const EdgeInsets.only(right: 20),
child: IconButton(
onPressed: () => setState(() => identifiersControllers.add(
Map<String, Object>.from({
'id': uuid.v4(),
'controller': TextEditingController()
})
)),
icon: const Icon(Icons.add)
),
)
],
),
if (identifiersControllers.isNotEmpty) ...identifiersControllers.map((controller) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextFormField(
enabled: editMode,
controller: controller['controller'],
onChanged: (_) => checkValidValues(),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.tag),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
helperText: AppLocalizations.of(context)!.identifierHelper,
labelText: AppLocalizations.of(context)!.identifier,
),
),
),
if (editMode == true) ...[
const SizedBox(width: 20),
Padding(
padding: const EdgeInsets.only(bottom: 25),
child: IconButton(
onPressed: () => setState(
() => identifiersControllers = identifiersControllers.where((e) => e['id'] != controller['id']).toList()
),
icon: const Icon(Icons.remove_circle_outline_outlined)
),
)
]
],
),
)).toList(),
if (identifiersControllers.isEmpty) Container(
padding: const EdgeInsets.only(top: 10),
child: Text(
AppLocalizations.of(context)!.noIdentifiers,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
sectionLabel(
label: AppLocalizations.of(context)!.settings,
padding: const EdgeInsets.only(
left: 24, right: 24, top: 12, bottom: 24
)
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Material(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(28),
child: InkWell(
onTap: editMode
? () => enableDisableGlobalSettingsFiltering()
: null,
borderRadius: BorderRadius.circular(28),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 5
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppLocalizations.of(context)!.useGlobalSettings,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface
),
),
Switch(
value: useGlobalSettingsFiltering,
onChanged: editMode == true
? (value) => enableDisableGlobalSettingsFiltering()
: null,
)
],
),
),
),
),
),
const SizedBox(height: 10),
settignsTile(
label: AppLocalizations.of(context)!.enableFiltering,
value: enableFiltering,
onChange: editMode == true
? (value) => setState(() => enableFiltering = value)
: null
),
settignsTile(
label: AppLocalizations.of(context)!.enableSafeBrowsing,
value: enableSafeBrowsing,
onChange: editMode == true
? (value) => setState(() => enableSafeBrowsing = value)
: null
),
settignsTile(
label: AppLocalizations.of(context)!.enableParentalControl,
value: enableParentalControl,
onChange: editMode == true
? (value) => setState(() => enableParentalControl = value)
: null
),
if (
serverVersionIsAhead(
currentVersion: statusProvider.serverStatus!.serverVersion,
referenceVersion: 'v0.107.28',
referenceVersionBeta: 'v0.108.0-b.33'
) == true
) CustomListTile(
title: AppLocalizations.of(context)!.safeSearch,
padding: const EdgeInsets.symmetric(
horizontal: 42,
vertical: 16
),
trailing: Padding(
padding: const EdgeInsets.only(right: 16),
child: Icon(
Icons.chevron_right_rounded,
color: useGlobalSettingsFiltering == true
? Colors.grey
: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
onTap: useGlobalSettingsFiltering == false
? () => openSafeSearchModal()
: null,
),
if (
serverVersionIsAhead(
currentVersion: statusProvider.serverStatus!.serverVersion,
referenceVersion: 'v0.107.28',
referenceVersionBeta: 'v0.108.0-b.33'
) == false
) settignsTile(
label: AppLocalizations.of(context)!.enableSafeSearch,
value: enableSafeSearch,
onChange: editMode == true
? (value) => setState(() => enableSafeSearch = value)
: null
),
sectionLabel(label: AppLocalizations.of(context)!.blockedServices),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Material(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(28),
child: InkWell(
onTap: editMode == true
? () => updateServicesGlobalSettings(!useGlobalSettingsServices)
: null,
borderRadius: BorderRadius.circular(28),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 5
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppLocalizations.of(context)!.useGlobalSettings,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface
),
),
Switch(
value: useGlobalSettingsServices,
onChanged: editMode == true
? (value) => updateServicesGlobalSettings(value)
: null,
)
],
),
),
),
),
),
const SizedBox(height: 10),
Material(
color: Colors.transparent,
child: InkWell(
onTap: editMode == true
? useGlobalSettingsServices == false
? openServicesModal
: null
: null,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8, horizontal: 24
),
child: Row(
children: [
Icon(
Icons.public,
color: useGlobalSettingsServices == false
? Theme.of(context).listTileTheme.iconColor
: Theme.of(context).colorScheme.onSurface.withOpacity(0.38),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.selectBlockedServices,
style: TextStyle(
fontSize: 16,
color: useGlobalSettingsServices == false
? Theme.of(context).colorScheme.onSurface
: Theme.of(context).colorScheme.onSurface.withOpacity(0.38),
),
),
if (useGlobalSettingsServices == false) ...[
const SizedBox(height: 5),
Text(
blockedServices.isNotEmpty
? "${blockedServices.length} ${AppLocalizations.of(context)!.servicesBlocked}"
: AppLocalizations.of(context)!.noBlockedServicesSelected,
style: TextStyle(
color: Theme.of(context).listTileTheme.iconColor
),
)
]
],
)
],
),
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
sectionLabel(label: AppLocalizations.of(context)!.upstreamServers),
if (editMode == true) Padding(
padding: const EdgeInsets.only(right: 20),
child: IconButton(
onPressed: () => setState(() => upstreamServers.add(
Map<String, Object>.from({
'id': uuid.v4(),
'controller': TextEditingController()
})
)),
icon: const Icon(Icons.add)
),
)
],
),
if (upstreamServers.isNotEmpty) ...upstreamServers.map((controller) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextFormField(
enabled: editMode,
controller: controller['controller'],
onChanged: (_) => checkValidValues(),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.dns_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
labelText: AppLocalizations.of(context)!.serverAddress,
),
),
),
if (editMode == true) ...[
const SizedBox(width: 20),
IconButton(
onPressed: () => setState(
() => upstreamServers = upstreamServers.where((e) => e['id'] != controller['id']).toList()
),
icon: const Icon(Icons.remove_circle_outline_outlined)
)
]
],
),
),
)).toList(),
if (upstreamServers.isEmpty) Container(
padding: const EdgeInsets.only(top: 12),
child: Column(
children: [
Text(
AppLocalizations.of(context)!.noUpstreamServers,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 10),
Text(
AppLocalizations.of(context)!.willBeUsedGeneralServers,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
const SizedBox(height: 20)
],
);
}
if (widget.dialog == true) {
return Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 500
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.clear_rounded)
),
const SizedBox(width: 8),
Text(
widget.client != null
? AppLocalizations.of(context)!.client
: AppLocalizations.of(context)!.addClient,
style: const TextStyle(
fontSize: 22
),
),
],
),
Row(
children: [
if (widget.client == null || (widget.client != null && editMode == true)) IconButton(
onPressed: validValues == true
? () {
createClient();
Navigator.pop(context);
}
: null,
icon: Icon(
widget.client != null && editMode == true
? Icons.save_rounded
: Icons.check_rounded
),
tooltip: widget.client != null && editMode == true
? AppLocalizations.of(context)!.save
: AppLocalizations.of(context)!.confirm,
),
if (widget.client != null && editMode == false) IconButton(
onPressed: () => setState(() => editMode = true),
icon: const Icon(Icons.edit_rounded),
tooltip: AppLocalizations.of(context)!.edit,
),
if (widget.client != null) IconButton(
onPressed: openDeleteClientScreen,
icon: const Icon(Icons.delete_rounded),
tooltip: AppLocalizations.of(context)!.delete,
),
const SizedBox(width: 10),
],
)
],
),
),
Flexible(
child: content(false)
)
],
),
),
);
}
else {
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close)
),
title: Text(
widget.client != null
? AppLocalizations.of(context)!.client
: AppLocalizations.of(context)!.addClient
),
actions: [
if (widget.client == null || (widget.client != null && editMode == true)) IconButton(
onPressed: validValues == true
? () {
createClient();
Navigator.pop(context);
}
: null,
icon: Icon(
widget.client != null && editMode == true
? Icons.save_rounded
: Icons.check_rounded
),
tooltip: widget.client != null && editMode == true
? AppLocalizations.of(context)!.save
: AppLocalizations.of(context)!.confirm,
),
if (widget.client != null && editMode == false) IconButton(
onPressed: () => setState(() => editMode = true),
icon: const Icon(Icons.edit_rounded),
tooltip: AppLocalizations.of(context)!.edit,
),
if (widget.client != null) IconButton(
onPressed: openDeleteClientScreen,
icon: const Icon(Icons.delete_rounded),
tooltip: AppLocalizations.of(context)!.delete,
),
const SizedBox(width: 10),
],
),
body: content(true)
);
}
}
}

View file

@ -7,7 +7,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/clients/clients_list.dart'; import 'package:adguard_home_manager/screens/clients/clients_list.dart';
import 'package:adguard_home_manager/screens/clients/search_clients.dart'; import 'package:adguard_home_manager/screens/clients/search_clients.dart';
import 'package:adguard_home_manager/screens/clients/logs_list_client.dart'; import 'package:adguard_home_manager/screens/clients/client/logs_list_client.dart';
import 'package:adguard_home_manager/screens/clients/clients_desktop_view.dart'; import 'package:adguard_home_manager/screens/clients/clients_desktop_view.dart';
import 'package:adguard_home_manager/screens/clients/added_list.dart'; import 'package:adguard_home_manager/screens/clients/added_list.dart';
@ -195,7 +195,8 @@ class _ClientsState extends State<Clients> with TickerProviderStateMixin {
clientsProvider.setSearchTermClients(null); clientsProvider.setSearchTermClients(null);
}); });
}, },
icon: const Icon(Icons.arrow_back_rounded) icon: const Icon(Icons.arrow_back_rounded),
tooltip: AppLocalizations.of(context)!.exitSearch,
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
@ -223,6 +224,7 @@ class _ClientsState extends State<Clients> with TickerProviderStateMixin {
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
fontSize: 18 fontSize: 18
), ),
autofocus: true,
), ),
) )
], ],

View file

@ -5,7 +5,7 @@ import 'package:flutter_split_view/flutter_split_view.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/clients/logs_list_client.dart'; import 'package:adguard_home_manager/screens/clients/client/logs_list_client.dart';
import 'package:adguard_home_manager/screens/clients/added_list.dart'; import 'package:adguard_home_manager/screens/clients/added_list.dart';
import 'package:adguard_home_manager/screens/clients/clients_list.dart'; import 'package:adguard_home_manager/screens/clients/clients_list.dart';

View file

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/clients/active_client_tile.dart'; import 'package:adguard_home_manager/screens/clients/client/active_client_tile.dart';
import 'package:adguard_home_manager/widgets/tab_content_list.dart'; import 'package:adguard_home_manager/widgets/tab_content_list.dart';

View file

@ -1,18 +1,16 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/clients/client_screen.dart'; import 'package:adguard_home_manager/screens/clients/client/client_screen_functions.dart';
import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/functions/snackbar.dart';
import 'package:adguard_home_manager/providers/status_provider.dart';
import 'package:adguard_home_manager/providers/clients_provider.dart'; import 'package:adguard_home_manager/providers/clients_provider.dart';
import 'package:adguard_home_manager/models/clients.dart'; import 'package:adguard_home_manager/models/clients.dart';
import 'package:adguard_home_manager/classes/process_modal.dart'; import 'package:adguard_home_manager/classes/process_modal.dart';
import 'package:adguard_home_manager/providers/status_provider.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart';
class ClientsFab extends StatelessWidget { class ClientsFab extends StatelessWidget {
@ -20,8 +18,8 @@ class ClientsFab extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final statusProvider = Provider.of<StatusProvider>(context); final statusProvider = Provider.of<StatusProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final clientsProvider = Provider.of<ClientsProvider>(context); final clientsProvider = Provider.of<ClientsProvider>(context);
final width = MediaQuery.of(context).size.width; final width = MediaQuery.of(context).size.width;
@ -51,32 +49,21 @@ class ClientsFab extends StatelessWidget {
} }
void openAddClient() { void openAddClient() {
if (width > 900 || !(Platform.isAndroid | Platform.isIOS)) { openClientFormModal(
showDialog(
barrierDismissible: false,
context: context, context: context,
builder: (BuildContext context) => ClientScreen( width: width,
onConfirm: confirmAddClient, onConfirm: confirmAddClient
serverVersion: statusProvider.serverStatus!.serverVersion,
dialog: true,
)
); );
} }
else {
Navigator.push(context, MaterialPageRoute(
fullscreenDialog: true,
builder: (BuildContext context) => ClientScreen(
onConfirm: confirmAddClient,
serverVersion: statusProvider.serverStatus!.serverVersion,
dialog: false,
)
));
}
}
if (statusProvider.serverStatus != null) {
return FloatingActionButton( return FloatingActionButton(
onPressed: openAddClient, onPressed: openAddClient,
child: const Icon(Icons.add), child: const Icon(Icons.add),
); );
} }
else {
return const SizedBox();
}
}
} }

View file

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/providers/status_provider.dart';
import 'package:adguard_home_manager/widgets/custom_list_tile_dialog.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile_dialog.dart';
class OptionsModal extends StatelessWidget { class OptionsModal extends StatelessWidget {
@ -15,6 +17,8 @@ class OptionsModal extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final statusProvider = Provider.of<StatusProvider>(context);
return AlertDialog( return AlertDialog(
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 0, horizontal: 0,
@ -39,7 +43,7 @@ class OptionsModal extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const SizedBox(height: 24), const SizedBox(height: 24),
CustomListTileDialog( if (statusProvider.serverStatus != null) CustomListTileDialog(
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
onEdit(); onEdit();

View file

@ -1,14 +1,12 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/clients/remove_client_modal.dart'; import 'package:adguard_home_manager/screens/clients/client/remove_client_modal.dart';
import 'package:adguard_home_manager/screens/clients/client_screen.dart'; import 'package:adguard_home_manager/screens/clients/client/client_screen_functions.dart';
import 'package:adguard_home_manager/screens/clients/options_modal.dart'; import 'package:adguard_home_manager/screens/clients/options_modal.dart';
import 'package:adguard_home_manager/widgets/section_label.dart'; import 'package:adguard_home_manager/widgets/section_label.dart';
@ -137,32 +135,13 @@ class _SearchClientsState extends State<SearchClients> {
} }
void openClientModal(Client client) { void openClientModal(Client client) {
if (width > 900 || !(Platform.isAndroid | Platform.isIOS)) { openClientFormModal(
showDialog(
barrierDismissible: false,
context: context, context: context,
builder: (BuildContext context) => ClientScreen( width: width,
onConfirm: confirmEditClient, onConfirm: confirmEditClient,
serverVersion: statusProvider.serverStatus!.serverVersion, onDelete: deleteClient
onDelete: deleteClient,
client: client,
dialog: true,
)
); );
} }
else {
Navigator.push(context, MaterialPageRoute(
fullscreenDialog: true,
builder: (BuildContext context) => ClientScreen(
onConfirm: confirmEditClient,
serverVersion: statusProvider.serverStatus!.serverVersion,
onDelete: deleteClient,
client: client,
dialog: false,
)
));
}
}
void openDeleteModal(Client client) { void openDeleteModal(Client client) {
showModal( showModal(
@ -252,7 +231,9 @@ class _SearchClientsState extends State<SearchClients> {
: const EdgeInsets.symmetric(horizontal: 20, vertical: 15), : const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
isThreeLine: true, isThreeLine: true,
onLongPress: () => openOptionsModal(clientsScreen[index]), onLongPress: () => openOptionsModal(clientsScreen[index]),
onTap: () => openClientModal(clientsScreen[index]), onTap: statusProvider.serverStatus != null
? () => openClientModal(clientsScreen[index])
: null,
title: Padding( title: Padding(
padding: const EdgeInsets.only(bottom: 5), padding: const EdgeInsets.only(bottom: 5),
child: Text( child: Text(

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:adguard_home_manager/widgets/add_server_modal.dart'; import 'package:adguard_home_manager/widgets/add_server/add_server_functions.dart';
import 'package:adguard_home_manager/widgets/version_warning_modal.dart';
class FabConnect extends StatelessWidget { class FabConnect extends StatelessWidget {
const FabConnect({Key? key}) : super(key: key); const FabConnect({Key? key}) : super(key: key);
@ -12,37 +11,7 @@ class FabConnect extends StatelessWidget {
void openAddServerModal() async { void openAddServerModal() async {
await Future.delayed(const Duration(seconds: 0), (() => { await Future.delayed(const Duration(seconds: 0), (() => {
if (width > 700) { openServerFormModal(context: context, width: width)
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AddServerModal(
window: true,
onUnsupportedVersion: (version) => showDialog(
context: context,
builder: (ctx) => VersionWarningModal(
version: version
),
barrierDismissible: false
),
),
)
}
else {
Navigator.push(context, MaterialPageRoute(
fullscreenDialog: true,
builder: (BuildContext context) => AddServerModal(
window: false,
onUnsupportedVersion: (version) => showDialog(
context: context,
builder: (ctx) => VersionWarningModal(
version: version
),
barrierDismissible: false
),
)
))
}
})); }));
} }

View file

@ -56,27 +56,30 @@ class AddFiltersButton extends StatelessWidget {
} }
void openAddCustomRule() { void openAddCustomRule() {
if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { showGeneralDialog(
showDialog(
context: context, context: context,
builder: (context) => AddCustomRule( barrierColor: !(width > 700 || !(Platform.isAndroid || Platform.isIOS))
onConfirm: confirmAddRule, ?Colors.transparent
dialog: true, : Colors.black54,
), transitionBuilder: (context, anim1, anim2, child) {
barrierDismissible: false return SlideTransition(
); position: Tween(
} begin: const Offset(0, 1),
else { end: const Offset(0, 0)
Navigator.of(context).push( ).animate(
MaterialPageRoute( CurvedAnimation(
fullscreenDialog: true, parent: anim1,
builder: (context) => AddCustomRule( curve: Curves.easeInOutCubicEmphasized
onConfirm: confirmAddRule,
dialog: false,
),
) )
),
child: child,
);
},
pageBuilder: (context, animation, secondaryAnimation) => AddCustomRule(
fullScreen: !(width > 700 || !(Platform.isAndroid || Platform.isIOS)),
onConfirm: confirmAddRule,
),
); );
}
} }
void confirmAddList({required String name, required String url, required String type}) async { void confirmAddList({required String name, required String url, required String type}) async {

View file

@ -6,12 +6,12 @@ import 'package:adguard_home_manager/constants/urls.dart';
class AddCustomRule extends StatefulWidget { class AddCustomRule extends StatefulWidget {
final void Function(String) onConfirm; final void Function(String) onConfirm;
final bool dialog; final bool fullScreen;
const AddCustomRule({ const AddCustomRule({
Key? key, Key? key,
required this.onConfirm, required this.onConfirm,
required this.dialog required this.fullScreen
}) : super(key: key); }) : super(key: key);
@override @override
@ -328,7 +328,32 @@ class _AddCustomRuleState extends State<AddCustomRule> {
]; ];
} }
if (widget.dialog == true) { if (widget.fullScreen == true) {
return Dialog.fullscreen(
child: Scaffold(
appBar: AppBar(
leading: CloseButton(onPressed: () => Navigator.pop(context)),
title: Text(AppLocalizations.of(context)!.addCustomRule),
actions: [
IconButton(
onPressed: checkValidValues() == true
? () {
Navigator.pop(context);
widget.onConfirm(buildRule());
}
: null,
icon: const Icon(Icons.check)
),
const SizedBox(width: 10)
],
),
body: ListView(
children: content(),
)
),
);
}
else {
return Dialog( return Dialog(
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints( constraints: const BoxConstraints(
@ -383,27 +408,5 @@ class _AddCustomRuleState extends State<AddCustomRule> {
), ),
); );
} }
else {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.addCustomRule),
actions: [
IconButton(
onPressed: checkValidValues() == true
? () {
Navigator.pop(context);
widget.onConfirm(buildRule());
}
: null,
icon: const Icon(Icons.check)
),
const SizedBox(width: 10)
],
),
body: ListView(
children: content(),
)
);
}
} }
} }

View file

@ -1,5 +1,7 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -12,11 +14,11 @@ import 'package:adguard_home_manager/providers/filtering_provider.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart';
class BlockedServicesScreen extends StatefulWidget { class BlockedServicesScreen extends StatefulWidget {
final bool dialog; final bool fullScreen;
const BlockedServicesScreen({ const BlockedServicesScreen({
Key? key, Key? key,
required this.dialog required this.fullScreen
}) : super(key: key); }) : super(key: key);
@override @override
@ -180,7 +182,40 @@ class _BlockedServicesScreenStateWidget extends State<BlockedServicesScreen> {
} }
} }
if (widget.dialog == true) { if (widget.fullScreen == true) {
return Dialog.fullscreen(
child: Scaffold(
appBar: AppBar(
leading: CloseButton(onPressed: () => Navigator.pop(context)),
title: Text(AppLocalizations.of(context)!.blockedServices),
actions: [
IconButton(
onPressed: updateBlockedServices,
icon: const Icon(
Icons.save_rounded
),
tooltip: AppLocalizations.of(context)!.save,
),
const SizedBox(width: 10)
],
),
body: RefreshIndicator(
onRefresh: () async {
final result = await filteringProvider.loadBlockedServices();
if (result == false) {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.blockedServicesListNotLoaded,
color: Colors.red
);
}
},
child: body()
),
),
);
}
else {
return Dialog( return Dialog(
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints( constraints: const BoxConstraints(
@ -228,35 +263,34 @@ class _BlockedServicesScreenStateWidget extends State<BlockedServicesScreen> {
), ),
); );
} }
else {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.blockedServices),
actions: [
IconButton(
onPressed: updateBlockedServices,
icon: const Icon(
Icons.save_rounded
),
tooltip: AppLocalizations.of(context)!.save,
),
const SizedBox(width: 10)
],
),
body: RefreshIndicator(
onRefresh: () async {
final result = await filteringProvider.loadBlockedServices();
if (result == false) {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.blockedServicesListNotLoaded,
color: Colors.red
);
}
},
child: body()
),
);
}
} }
} }
void openBlockedServicesModal({
required BuildContext context,
required double width,
}) {
showGeneralDialog(
context: context,
barrierColor: !(width > 700 || !(Platform.isAndroid || Platform.isIOS))
?Colors.transparent
: Colors.black54,
transitionBuilder: (context, anim1, anim2, child) {
return SlideTransition(
position: Tween(
begin: const Offset(0, 1),
end: const Offset(0, 0)
).animate(
CurvedAnimation(
parent: anim1,
curve: Curves.easeInOutCubicEmphasized
)
),
child: child,
);
},
pageBuilder: (context, animation, secondaryAnimation) => BlockedServicesScreen(
fullScreen: !(width > 700 || !(Platform.isAndroid || Platform.isIOS)),
),
);
}

View file

@ -149,26 +149,9 @@ class _FiltersState extends State<Filters> {
} }
} }
void openBlockedServicesModal() { void openBlockedServices() {
Future.delayed(const Duration(seconds: 0), () { Future.delayed(const Duration(seconds: 0), () {
if (width > 700 || !(Platform.isAndroid || Platform.isIOS)) { openBlockedServicesModal(context: context, width: width);
showDialog(
context: context,
builder: (context) => const BlockedServicesScreen(
dialog: true,
),
barrierDismissible: false
);
}
else {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const BlockedServicesScreen(
dialog: false,
),
)
);
}
}); });
} }
@ -312,7 +295,7 @@ class _FiltersState extends State<Filters> {
) )
), ),
PopupMenuItem( PopupMenuItem(
onTap: openBlockedServicesModal, onTap: openBlockedServices,
child: Row( child: Row(
children: [ children: [
const Icon(Icons.block), const Icon(Icons.block),

View file

@ -276,7 +276,8 @@ class FiltersTripleColumn extends StatelessWidget {
subtitleWidget: generateSubtitle(filteringProvider.filtering!.userRules[index]), subtitleWidget: generateSubtitle(filteringProvider.filtering!.userRules[index]),
trailing: IconButton( trailing: IconButton(
onPressed: () => onRemoveCustomRule(filteringProvider.filtering!.userRules[index]), onPressed: () => onRemoveCustomRule(filteringProvider.filtering!.userRules[index]),
icon: const Icon(Icons.delete) icon: const Icon(Icons.delete),
tooltip: AppLocalizations.of(context)!.delete,
), ),
), ),
), ),

View file

@ -76,15 +76,20 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
// ------- // // ------- //
} }
void updateList(FilteringListActions action) async { void updateList({
required FilteringListActions action,
required Filter filterList,
}) async {
ProcessModal processModal = ProcessModal(context: context); ProcessModal processModal = ProcessModal(context: context);
processModal.open( processModal.open(
list!.enabled == true action == FilteringListActions.edit
? AppLocalizations.of(context)!.savingList
: action == FilteringListActions.disable
? AppLocalizations.of(context)!.disablingList ? AppLocalizations.of(context)!.disablingList
: AppLocalizations.of(context)!.enablingList, : AppLocalizations.of(context)!.enablingList,
); );
final result = await filteringProvider.updateList( final result = await filteringProvider.updateList(
list: list, list: filterList,
type: widget.type, type: widget.type,
action: action action: action
); );
@ -204,7 +209,8 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
list: list, list: list,
type: widget.type, type: widget.type,
onEdit: ({required Filter list, required String type}) async => updateList( onEdit: ({required Filter list, required String type}) async => updateList(
FilteringListActions.edit action: FilteringListActions.edit,
filterList: list
), ),
dialog: true, dialog: true,
), ),
@ -217,7 +223,8 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
list: list, list: list,
type: widget.type, type: widget.type,
onEdit: ({required Filter list, required String type}) async => updateList( onEdit: ({required Filter list, required String type}) async => updateList(
FilteringListActions.edit action: FilteringListActions.edit,
filterList: list
), ),
dialog: false, dialog: false,
), ),
@ -302,9 +309,10 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
children: [ children: [
IconButton( IconButton(
onPressed: () => updateList( onPressed: () => updateList(
list!.enabled == true action: list!.enabled == true
? FilteringListActions.disable ? FilteringListActions.disable
: FilteringListActions.enable : FilteringListActions.enable,
filterList: list
), ),
icon: Icon( icon: Icon(
list.enabled == true list.enabled == true
@ -371,9 +379,10 @@ class _ListDetailsScreenState extends State<ListDetailsScreen> {
right: 20, right: 20,
child: FloatingActionButton( child: FloatingActionButton(
onPressed: () => updateList( onPressed: () => updateList(
list!.enabled == true action: list!.enabled == true
? FilteringListActions.disable ? FilteringListActions.disable
: FilteringListActions.enable : FilteringListActions.enable,
filterList: list
), ),
child: Icon( child: Icon(
list.enabled == true list.enabled == true

View file

@ -94,19 +94,7 @@ class _UpdateIntervalListsModalState extends State<UpdateIntervalListsModal> {
optionsValue: selectedOption, optionsValue: selectedOption,
itemValue: 0, itemValue: 0,
onTap: _updateRadioValue, onTap: _updateRadioValue,
child: Center( label: AppLocalizations.of(context)!.never,
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 250),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: selectedOption == 0
? Theme.of(context).colorScheme.onInverseSurface
: Theme.of(context).colorScheme.onSurface
),
child: Text(AppLocalizations.of(context)!.never),
),
),
), ),
), ),
), ),
@ -118,19 +106,7 @@ class _UpdateIntervalListsModalState extends State<UpdateIntervalListsModal> {
optionsValue: selectedOption, optionsValue: selectedOption,
itemValue: 1, itemValue: 1,
onTap: _updateRadioValue, onTap: _updateRadioValue,
child: Center( label: AppLocalizations.of(context)!.hour1,
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 250),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: selectedOption == 1
? Theme.of(context).colorScheme.onInverseSurface
: Theme.of(context).colorScheme.onSurface
),
child: Text(AppLocalizations.of(context)!.hour1),
),
),
), ),
), ),
), ),
@ -142,19 +118,7 @@ class _UpdateIntervalListsModalState extends State<UpdateIntervalListsModal> {
optionsValue: selectedOption, optionsValue: selectedOption,
itemValue: 12, itemValue: 12,
onTap: _updateRadioValue, onTap: _updateRadioValue,
child: Center( label: AppLocalizations.of(context)!.hours12,
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 250),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: selectedOption == 12
? Theme.of(context).colorScheme.onInverseSurface
: Theme.of(context).colorScheme.onSurface
),
child: Text(AppLocalizations.of(context)!.hours12),
),
),
), ),
), ),
), ),
@ -166,19 +130,8 @@ class _UpdateIntervalListsModalState extends State<UpdateIntervalListsModal> {
optionsValue: selectedOption, optionsValue: selectedOption,
itemValue: 24, itemValue: 24,
onTap: _updateRadioValue, onTap: _updateRadioValue,
child: Center( label: AppLocalizations.of(context)!.hours24,
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 250),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: selectedOption == 24
? Theme.of(context).colorScheme.onInverseSurface
: Theme.of(context).colorScheme.onSurface
),
child: Text(AppLocalizations.of(context)!.hours24),
),
),
), ),
), ),
), ),
@ -190,19 +143,8 @@ class _UpdateIntervalListsModalState extends State<UpdateIntervalListsModal> {
optionsValue: selectedOption, optionsValue: selectedOption,
itemValue: 72, itemValue: 72,
onTap: _updateRadioValue, onTap: _updateRadioValue,
child: Center( label: AppLocalizations.of(context)!.days3,
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 250),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: selectedOption == 72
? Theme.of(context).colorScheme.onInverseSurface
: Theme.of(context).colorScheme.onSurface
),
child: Text(AppLocalizations.of(context)!.days3),
),
),
), ),
), ),
), ),
@ -214,19 +156,7 @@ class _UpdateIntervalListsModalState extends State<UpdateIntervalListsModal> {
optionsValue: selectedOption, optionsValue: selectedOption,
itemValue: 168, itemValue: 168,
onTap: _updateRadioValue, onTap: _updateRadioValue,
child: Center( label: AppLocalizations.of(context)!.days7,
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 250),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: selectedOption == 168
? Theme.of(context).colorScheme.onInverseSurface
: Theme.of(context).colorScheme.onSurface
),
child: Text(AppLocalizations.of(context)!.days7),
),
),
), ),
), ),
), ),

View file

@ -6,9 +6,8 @@ import 'package:flutter/rendering.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:adguard_home_manager/widgets/version_warning_modal.dart'; import 'package:adguard_home_manager/widgets/add_server/add_server_functions.dart';
import 'package:adguard_home_manager/widgets/servers_list/servers_list.dart'; import 'package:adguard_home_manager/widgets/servers_list/servers_list.dart';
import 'package:adguard_home_manager/widgets/add_server_modal.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart'; import 'package:adguard_home_manager/providers/servers_provider.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart';
@ -69,37 +68,7 @@ class _ServersState extends State<Servers> {
void openAddServerModal() async { void openAddServerModal() async {
await Future.delayed(const Duration(seconds: 0), (() => { await Future.delayed(const Duration(seconds: 0), (() => {
if (width > 700) { openServerFormModal(context: context, width: width)
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AddServerModal(
window: true,
onUnsupportedVersion: (version) => showDialog(
context: context,
builder: (ctx) => VersionWarningModal(
version: version
),
barrierDismissible: false
),
),
)
}
else {
Navigator.push(context, MaterialPageRoute(
fullscreenDialog: true,
builder: (BuildContext context) => AddServerModal(
window: false,
onUnsupportedVersion: (version) => showDialog(
context: context,
builder: (ctx) => VersionWarningModal(
version: version
),
barrierDismissible: false
),
)
))
}
})); }));
} }

View file

@ -8,7 +8,7 @@ import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/settings/access_settings/add_client_modal.dart'; import 'package:adguard_home_manager/screens/settings/access_settings/add_client_modal.dart';
import 'package:adguard_home_manager/screens/clients/remove_client_modal.dart'; import 'package:adguard_home_manager/screens/clients/client/remove_client_modal.dart';
import 'package:adguard_home_manager/widgets/tab_content_list.dart'; import 'package:adguard_home_manager/widgets/tab_content_list.dart';
import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/functions/snackbar.dart';

View file

@ -345,7 +345,9 @@ class _DhcpScreenState extends State<DhcpScreen> {
}), }),
dialog: false, dialog: false,
), ),
isScrollControlled: true isScrollControlled: true,
useSafeArea: true,
backgroundColor: Colors.transparent
); );
} }
}); });

View file

@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/models/dhcp.dart';
class DhcpInterfaceItem extends StatelessWidget {
final NetworkInterface networkInterface;
final void Function(NetworkInterface) onSelect;
const DhcpInterfaceItem({
Key? key,
required this.networkInterface,
required this.onSelect
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
Navigator.pop(context);
onSelect(networkInterface);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
networkInterface.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface
),
),
Row(
children: [
Text(
"${AppLocalizations.of(context)!.hardwareAddress}: ",
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
Text(
networkInterface.hardwareAddress,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
],
),
const SizedBox(height: 5),
if (networkInterface.flags.isNotEmpty) ...[
Row(
children: [
Text(
"Flags: ",
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
Text(
networkInterface.flags.join(', '),
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
],
),
const SizedBox(height: 5),
],
if (networkInterface.gatewayIp != null && networkInterface.gatewayIp != '') ...[
Row(
children: [
Text(
"${AppLocalizations.of(context)!.gatewayIp}: ",
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
Text(
networkInterface.gatewayIp!,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
],
),
const SizedBox(height: 5),
],
if (networkInterface.ipv4Addresses.isNotEmpty) ...[
Row(
children: [
Flexible(
child: Text(
"${AppLocalizations.of(context)!.ipv4addresses}: ${networkInterface.ipv4Addresses.join(', ')}",
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
)
],
),
const SizedBox(height: 5),
],
if (networkInterface.ipv6Addresses.isNotEmpty) ...[
Row(
children: [
Flexible(
child: Text(
"${AppLocalizations.of(context)!.ipv6addresses}: ${networkInterface.ipv6Addresses.join(', ')}",
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
)
],
),
]
],
),
),
),
);
}
}

View file

@ -3,6 +3,8 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/settings/dhcp/dhcp_interface_item.dart';
import 'package:adguard_home_manager/models/dhcp.dart'; import 'package:adguard_home_manager/models/dhcp.dart';
class SelectInterfaceModal extends StatelessWidget { class SelectInterfaceModal extends StatelessWidget {
@ -19,12 +21,17 @@ class SelectInterfaceModal extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget content() { if (dialog == true) {
return Column( return Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 500,
),
child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Flexible( Padding(
child: SingleChildScrollView( padding: const EdgeInsets.all(16),
child: Wrap( child: Wrap(
children: [ children: [
Row( Row(
@ -32,14 +39,11 @@ class SelectInterfaceModal extends StatelessWidget {
children: [ children: [
Column( Column(
children: [ children: [
Padding( Icon(
padding: const EdgeInsets.only(top: 24),
child: Icon(
Icons.settings_ethernet_rounded, Icons.settings_ethernet_rounded,
size: 24, size: 24,
color: Theme.of(context).listTileTheme.iconColor color: Theme.of(context).listTileTheme.iconColor
), ),
),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
AppLocalizations.of(context)!.selectInterface, AppLocalizations.of(context)!.selectInterface,
@ -54,130 +58,16 @@ class SelectInterfaceModal extends StatelessWidget {
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
ListView.builder( ],
primary: false, ),
shrinkWrap: true, ),
Expanded(
child: ListView.builder(
itemCount: interfaces.length, itemCount: interfaces.length,
itemBuilder: (context, index) => Material( itemBuilder: (context, index) => DhcpInterfaceItem(
color: Colors.transparent, networkInterface: interfaces[index],
child: InkWell( onSelect: onSelect
onTap: () {
Navigator.pop(context);
onSelect(interfaces[index]);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
interfaces[index].name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface
),
),
Row(
children: [
Text(
"${AppLocalizations.of(context)!.hardwareAddress}: ",
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
Text(
interfaces[index].hardwareAddress,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
],
),
const SizedBox(height: 5),
if (interfaces[index].flags.isNotEmpty) ...[
Row(
children: [
Text(
"Flags: ",
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
Text(
interfaces[index].flags.join(', '),
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
],
),
const SizedBox(height: 5),
],
if (interfaces[index].gatewayIp != null && interfaces[index].gatewayIp != '') ...[
Row(
children: [
Text(
"${AppLocalizations.of(context)!.gatewayIp}: ",
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
Text(
interfaces[index].gatewayIp!,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
],
),
const SizedBox(height: 5),
],
if (interfaces[index].ipv4Addresses.isNotEmpty) ...[
Row(
children: [
Flexible(
child: Text(
"${AppLocalizations.of(context)!.ipv4addresses}: ${interfaces[index].ipv4Addresses.join(', ')}",
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
) )
],
),
const SizedBox(height: 5),
],
if (interfaces[index].ipv6Addresses.isNotEmpty) ...[
Row(
children: [
Flexible(
child: Text(
"${AppLocalizations.of(context)!.ipv6addresses}: ${interfaces[index].ipv6Addresses.join(', ')}",
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
)
],
),
]
],
),
),
),
)
),
],
),
), ),
), ),
Padding( Padding(
@ -194,29 +84,77 @@ class SelectInterfaceModal extends StatelessWidget {
), ),
if (Platform.isIOS) const SizedBox(height: 16) if (Platform.isIOS) const SizedBox(height: 16)
], ],
);
}
if (dialog == true) {
return Dialog(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 500
), ),
child: content()
), ),
); );
} }
else { else {
return GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
color: Colors.transparent,
child: DraggableScrollableSheet(
initialChildSize: 0.6,
minChildSize: 0.3,
maxChildSize: 1,
builder: (context, controller) {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).dialogBackgroundColor, color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28), topLeft: Radius.circular(28),
topRight: Radius.circular(28) topRight: Radius.circular(28),
),
),
child: Column(
children: [
Container(
margin: const EdgeInsets.all(16),
width: 36,
height: 4,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.grey
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
children: [
Icon(
Icons.settings_ethernet_rounded,
size: 24,
color: Theme.of(context).listTileTheme.iconColor
),
const SizedBox(height: 16),
Text(
AppLocalizations.of(context)!.selectInterface,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
color: Theme.of(context).colorScheme.onSurface
),
),
],
),
),
Expanded(
child: ListView.builder(
controller: controller,
itemCount: interfaces.length,
itemBuilder: (context, index) => DhcpInterfaceItem(
networkInterface: interfaces[index],
onSelect: onSelect
)
) )
), ),
child: content() const SizedBox(height: 16)
],
),
);
},
),
),
); );
} }
} }

View file

@ -79,7 +79,7 @@ class _PrivateReverseDnsServersScreenState extends State<PrivateReverseDnsServer
editReverseResolvers = true; editReverseResolvers = true;
} }
usePrivateReverseDnsResolvers = dnsProvider.dnsInfo!.usePrivatePtrResolvers; usePrivateReverseDnsResolvers = dnsProvider.dnsInfo!.usePrivatePtrResolvers;
enableReverseResolve = dnsProvider.dnsInfo!.resolveClients; enableReverseResolve = dnsProvider.dnsInfo!.resolveClients ?? false;
validValues = true; validValues = true;
super.initState(); super.initState();
} }

View file

@ -85,6 +85,7 @@ class _TopItemsScreenState extends State<TopItemsScreen> {
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
fontSize: 18 fontSize: 18
), ),
autofocus: true,
), ),
) )
: Text(widget.title), : Text(widget.title),

View file

@ -616,6 +616,7 @@ class ApiClient {
} }
Future getClients() async { Future getClients() async {
try {
final result = await Future.wait([ final result = await Future.wait([
apiRequest(server: server, method: 'get', urlPath: '/clients', type: 'get_clients'), apiRequest(server: server, method: 'get', urlPath: '/clients', type: 'get_clients'),
apiRequest(server: server, method: 'get', urlPath: '/access/list', type: 'get_clients'), apiRequest(server: server, method: 'get', urlPath: '/access/list', type: 'get_clients'),
@ -655,6 +656,18 @@ class ApiClient {
) )
}; };
} }
} catch (e) {
Sentry.captureException(e);
return {
'result': 'error',
'log': AppLog(
type: 'get_clients',
dateTime: DateTime.now(),
message: 'no_response',
resBody: e.toString()
)
};
}
} }
Future requestAllowedBlockedClientsHosts(Map<String, List<String>?> body) async { Future requestAllowedBlockedClientsHosts(Map<String, List<String>?> body) async {
@ -701,6 +714,7 @@ class ApiClient {
String? responseStatus, String? responseStatus,
String? search String? search
}) async { }) async {
try {
final result = await apiRequest( final result = await apiRequest(
server: server, server: server,
method: 'get', method: 'get',
@ -731,9 +745,22 @@ class ApiClient {
else { else {
return result; return result;
} }
} catch (e) {
Sentry.captureException(e);
return {
'result': 'error',
'log': AppLog(
type: 'get_logs',
dateTime: DateTime.now(),
message: 'error_code_not_expected',
resBody: e.toString()
)
};
}
} }
Future getFilteringRules() async { Future getFilteringRules() async {
try {
final result = await apiRequest( final result = await apiRequest(
server: server, server: server,
method: 'get', method: 'get',
@ -764,6 +791,18 @@ class ApiClient {
else { else {
return result; return result;
} }
} catch (e) {
Sentry.captureException(e);
return {
'result': 'error',
'log': AppLog(
type: 'get_filtering_rules',
dateTime: DateTime.now(),
message: 'error_code_not_expected',
resBody: e.toString()
)
};
}
} }
Future postFilteringRules({ Future postFilteringRules({
@ -899,6 +938,7 @@ class ApiClient {
} }
Future getFiltering() async { Future getFiltering() async {
try {
final result = await Future.wait([ final result = await Future.wait([
apiRequest( apiRequest(
urlPath: '/filtering/status', urlPath: '/filtering/status',
@ -951,6 +991,18 @@ class ApiClient {
) )
}; };
} }
} catch (e) {
Sentry.captureException(e);
return {
'result': 'error',
'log': AppLog(
type: 'get_filtering_status',
dateTime: DateTime.now(),
message: 'no_response',
resBody: e.toString(),
)
};
}
} }
Future setCustomRules({ Future setCustomRules({

View file

@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/models/server.dart';
import 'package:adguard_home_manager/widgets/add_server/add_server_modal.dart';
import 'package:adguard_home_manager/widgets/version_warning_modal.dart';
bool checkDataValid({
required TextEditingController nameController,
required TextEditingController ipDomainController,
required String? ipDomainError,
required String? pathError,
required String? portError,
}) {
if (
nameController.text != '' &&
ipDomainController.text != '' &&
ipDomainError == null &&
pathError == null &&
portError == null
) {
return true;
}
else {
return false;
}
}
String? validatePort({
required String? value,
required BuildContext context
}) {
if (value != null && value != '') {
if (int.tryParse(value) != null && int.parse(value) <= 65535) {
return null;
}
else {
return AppLocalizations.of(context)!.invalidPort;
}
}
else {
return null;
}
}
String? validateSubroute({
required BuildContext context,
required String? value
}) {
if (value != null && value != '') {
RegExp subrouteRegexp = RegExp(r'^\/\b([A-Za-z0-9_\-~/]*)[^\/|\.|\:]$');
if (subrouteRegexp.hasMatch(value) == true) {
return null;
}
else {
return AppLocalizations.of(context)!.invalidPath;
}
}
else {
return null;
}
}
String? validateAddress({
required BuildContext context,
required String? value
}) {
if (value != null && value != '') {
RegExp ipAddress = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$');
RegExp domain = RegExp(r'^(([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+)$');
if (ipAddress.hasMatch(value) == true || domain.hasMatch(value) == true) {
return null;
}
else {
return AppLocalizations.of(context)!.invalidIpDomain;
}
}
else {
return AppLocalizations.of(context)!.ipDomainNotEmpty;
}
}
void openServerFormModal({
required BuildContext context,
required double width,
Server? server,
}) {
showGeneralDialog(
context: context,
barrierColor: width <= 700
?Colors.transparent
: Colors.black54,
transitionBuilder: (context, anim1, anim2, child) {
return SlideTransition(
position: Tween(
begin: const Offset(0, 1),
end: const Offset(0, 0)
).animate(
CurvedAnimation(
parent: anim1,
curve: Curves.easeInOutCubicEmphasized
)
),
child: child,
);
},
pageBuilder: (context, animation, secondaryAnimation) => AddServerModal(
fullScreen: width <= 700,
server: server,
onUnsupportedVersion: (version) => showDialog(
context: context,
builder: (ctx) => VersionWarningModal(
version: version
),
barrierDismissible: false
),
),
);
}

View file

@ -0,0 +1,543 @@
// ignore_for_file: use_build_context_synchronously
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/widgets/add_server/form_text_field.dart';
import 'package:adguard_home_manager/widgets/section_label.dart';
import 'package:adguard_home_manager/widgets/custom_switch_list_tile.dart';
import 'package:adguard_home_manager/widgets/add_server/add_server_functions.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/functions/snackbar.dart';
import 'package:adguard_home_manager/constants/urls.dart';
import 'package:adguard_home_manager/functions/open_url.dart';
import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/providers/status_provider.dart';
import 'package:adguard_home_manager/functions/base64.dart';
import 'package:adguard_home_manager/services/http_requests.dart';
import 'package:adguard_home_manager/models/app_log.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
import 'package:adguard_home_manager/models/server.dart';
enum ConnectionType { http, https}
class AddServerModal extends StatefulWidget {
final Server? server;
final bool fullScreen;
final void Function(String version) onUnsupportedVersion;
const AddServerModal({
Key? key,
this.server,
required this.fullScreen,
required this.onUnsupportedVersion
}) : super(key: key);
@override
State<AddServerModal> createState() => _AddServerModalState();
}
class _AddServerModalState extends State<AddServerModal> {
final uuid = const Uuid();
final TextEditingController nameController = TextEditingController();
String? nameError;
ConnectionType connectionType = 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;
bool homeAssistant = false;
bool allDataValid = false;
bool isConnecting = false;
@override
void initState() {
if (widget.server != null) {
nameController.text = widget.server!.name;
connectionType = widget.server!.connectionMethod == 'https' ? ConnectionType.https : ConnectionType.http;
ipDomainController.text = widget.server!.domain;
pathController.text = widget.server!.path ?? '';
portController.text = widget.server!.port != null ? widget.server!.port.toString() : "";
userController.text = widget.server!.user ?? "";
passwordController.text = widget.server!.password ?? "";
defaultServer = widget.server!.defaultServer;
homeAssistant = widget.server!.runningOnHa;
}
setState(() => allDataValid = checkDataValid(
ipDomainController: ipDomainController,
nameController: nameController,
ipDomainError: ipDomainError,
pathError: pathError,
portError: portError
));
super.initState();
}
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context, listen: false);
final statusProvider = Provider.of<StatusProvider>(context, listen: false);
final appConfigProvider = Provider.of<AppConfigProvider>(context, listen: false);
void cancelConnecting() {
if (mounted) {
setState(() => isConnecting = false);
}
else {
isConnecting = false;
}
}
void validateData() {
setState(() => allDataValid = checkDataValid(
ipDomainController: ipDomainController,
nameController: nameController,
ipDomainError: ipDomainError,
pathError: pathError,
portError: portError
));
}
String getErrorMessage(String message) {
if (message == 'invalid_username_password') return AppLocalizations.of(context)!.invalidUsernamePassword;
if (message == 'many_attempts') return AppLocalizations.of(context)!.tooManyAttempts;
if (message == 'no_connection') return AppLocalizations.of(context)!.cantReachServer;
if (message == 'server_error') return AppLocalizations.of(context)!.serverError;
return AppLocalizations.of(context)!.unknownError;
}
void connect() async {
Server serverObj = Server(
id: uuid.v4(),
name: nameController.text,
connectionMethod: connectionType.name,
domain: ipDomainController.text,
port: portController.text != '' ? int.parse(portController.text) : null,
user: userController.text != "" ? userController.text : null,
password: passwordController.text != "" ? passwordController.text : null,
defaultServer: defaultServer,
authToken: homeAssistant == true
? encodeBase64UserPass(userController.text, passwordController.text)
: null,
runningOnHa: homeAssistant
);
setState(() => isConnecting = true);
final result = homeAssistant == true
? await loginHA(serverObj)
: await login(serverObj);
// If something goes wrong with the connection
if (result['result'] != 'success') {
cancelConnecting();
appConfigProvider.addLog(result['log']);
if (mounted) {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: getErrorMessage(result['result']),
color: Colors.red
);
}
return;
}
if (serverObj.user != null && serverObj.password != null) {
serverObj.authToken = encodeBase64UserPass(serverObj.user!, serverObj.password!);
}
final serverCreated = await serversProvider.createServer(serverObj);
// If something goes wrong when saving the connection on the db
if (serverCreated != null) {
if (mounted) setState(() => isConnecting = false);
appConfigProvider.addLog(
AppLog(
type: 'save_connection_db',
dateTime: DateTime.now(),
message: serverCreated.toString()
)
);
if (mounted) {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.connectionNotCreated,
color: Colors.red
);
}
return;
}
statusProvider.setServerStatusLoad(LoadStatus.loading);
final ApiClient apiClient = ApiClient(server: serverObj);
final serverStatus = await apiClient.getServerStatus();
// If something goes wrong when fetching server status
if (serverStatus['result'] != 'success') {
appConfigProvider.addLog(serverStatus['log']);
statusProvider.setServerStatusLoad(LoadStatus.error);
Navigator.pop(context);
return;
}
// If everything is successful
statusProvider.setServerStatusData(
data: serverStatus['data']
);
serversProvider.setApiClient(apiClient);
statusProvider.setServerStatusLoad(LoadStatus.loaded);
if (serverStatus['data'].serverVersion.contains('a') || serverStatus['data'].serverVersion.contains('b')) {
Navigator.pop(context);
widget.onUnsupportedVersion(serverStatus['data'].serverVersion);
}
else {
Navigator.pop(context);
}
return;
}
void edit() async {
final Server serverObj = Server(
id: widget.server!.id,
name: nameController.text,
connectionMethod: connectionType.name,
domain: ipDomainController.text,
port: portController.text != '' ? int.parse(portController.text) : null,
user: userController.text != "" ? userController.text : null,
password: passwordController.text != "" ? passwordController.text : null,
defaultServer: defaultServer,
authToken: homeAssistant == true
? encodeBase64UserPass(userController.text, passwordController.text)
: null,
runningOnHa: homeAssistant
);
final result = homeAssistant == true
? await loginHA(serverObj)
: await login(serverObj);
// If something goes wrong with the connection
if (result['result'] != 'success') {
cancelConnecting();
appConfigProvider.addLog(result['log']);
if (mounted) {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: getErrorMessage(result['result']),
color: Colors.red
);
}
return;
}
if (serverObj.user != null && serverObj.password != null) {
serverObj.authToken = encodeBase64UserPass(serverObj.user!, serverObj.password!);
}
final serverSaved = await serversProvider.editServer(serverObj);
// If something goes wrong when saving the connection on the db
if (serverSaved != null) {
if (mounted) setState(() => isConnecting = false);
appConfigProvider.addLog(
AppLog(
type: 'save_connection_db',
dateTime: DateTime.now(),
message: serverSaved.toString()
)
);
if (mounted) {
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.connectionNotCreated,
color: Colors.red
);
}
return;
}
// If everything is successful
final ApiClient apiClient = ApiClient(server: serverObj);
final version = await apiClient.getServerVersion();
if (
version['result'] == 'success' &&
(version['data'].contains('a') || version['data'].contains('b')) // alpha or beta
) {
Navigator.pop(context);
widget.onUnsupportedVersion(version['data']);
}
else {
Navigator.pop(context);
}
return;
}
Widget actions() {
return Row(
children: [
IconButton(
onPressed: () => openUrl(Urls.connectionInstructions),
icon: const Icon(Icons.help_outline_outlined)
),
IconButton(
tooltip: widget.server == null
? AppLocalizations.of(context)!.connect
: AppLocalizations.of(context)!.save,
onPressed: allDataValid == true && isConnecting == false
? widget.server == null
? () => connect()
: () => edit()
: null,
icon: isConnecting
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator()
)
: Icon(
widget.server == null
? Icons.login_rounded
: Icons.save_rounded
)
),
],
);
}
List<Widget> form() {
return [
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
margin: const EdgeInsets.only(
top: 24,
left: 24,
right: 24
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.05),
borderRadius: BorderRadius.circular(30),
border: Border.all(
color: Theme.of(context).colorScheme.primary
)
),
child: Text(
"${connectionType.name}://${ipDomainController.text}${portController.text != '' ? ':${portController.text}' : ""}${pathController.text}",
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500
),
),
),
SectionLabel(
label: AppLocalizations.of(context)!.general,
padding: const EdgeInsets.all(24),
),
FormTextField(
label: AppLocalizations.of(context)!.name,
controller: nameController,
icon: Icons.badge_rounded,
error: nameError,
onChanged: (value) {
if (value != '') {
setState(() => nameError = null);
}
else {
setState(() => nameError = AppLocalizations.of(context)!.nameNotEmpty);
}
validateData();
},
isConnecting: isConnecting,
),
SectionLabel(
label: AppLocalizations.of(context)!.connection,
padding: const EdgeInsets.all(24),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: SegmentedButton<ConnectionType>(
segments: const [
ButtonSegment(
value: ConnectionType.http,
label: Text("HTTP")
),
ButtonSegment(
value: ConnectionType.https,
label: Text("HTTPS")
),
],
selected: <ConnectionType>{connectionType},
onSelectionChanged: (value) => setState(() => connectionType = value.first),
),
),
const SizedBox(height: 30),
FormTextField(
label: AppLocalizations.of(context)!.ipDomain,
controller: ipDomainController,
icon: Icons.link_rounded,
error: ipDomainError,
keyboardType: TextInputType.url,
onChanged: (v) {
setState(() => ipDomainError = validateAddress(context: context, value: v));
validateData();
},
isConnecting: isConnecting,
),
const SizedBox(height: 20),
FormTextField(
label: AppLocalizations.of(context)!.path,
controller: pathController,
icon: Icons.route_rounded,
error: pathError,
onChanged: (v) {
setState(() => pathError = validateSubroute(context: context, value: v));
validateData();
},
hintText: AppLocalizations.of(context)!.examplePath,
helperText: AppLocalizations.of(context)!.helperPath,
isConnecting: isConnecting,
),
const SizedBox(height: 20),
FormTextField(
label: AppLocalizations.of(context)!.port,
controller: portController,
icon: Icons.numbers_rounded,
error: portError,
keyboardType: TextInputType.number,
onChanged: (v) {
setState(() => portError = validatePort(context: context, value: v));
validateData();
},
isConnecting: isConnecting,
),
SectionLabel(
label: AppLocalizations.of(context)!.authentication,
padding: const EdgeInsets.all(24),
),
FormTextField(
label: AppLocalizations.of(context)!.username,
controller: userController,
icon: Icons.person_rounded,
isConnecting: isConnecting,
),
const SizedBox(height: 20),
FormTextField(
label: AppLocalizations.of(context)!.password,
controller: passwordController,
icon: Icons.lock_rounded,
keyboardType: TextInputType.visiblePassword,
obscureText: true,
isConnecting: isConnecting,
),
SectionLabel(
label: AppLocalizations.of(context)!.other,
padding: const EdgeInsets.only(
top: 32,
left: 24,
bottom: 12
),
),
CustomSwitchListTile(
value: defaultServer,
onChanged: (value) => setState(() => defaultServer = value),
title: AppLocalizations.of(context)!.defaultServer,
disabled: widget.server != null || isConnecting,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 4
),
),
CustomSwitchListTile(
value: homeAssistant,
onChanged: (value) => setState(() => homeAssistant = value),
title: AppLocalizations.of(context)!.runningHomeAssistant,
disabled: widget.server != null || isConnecting,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 4
),
),
const SizedBox(height: 20),
];
}
if (widget.fullScreen == true) {
return Dialog.fullscreen(
child: Scaffold(
appBar: AppBar(
leading: CloseButton(
onPressed: () => Navigator.pop(context),
),
title: widget.server != null
? Text(AppLocalizations.of(context)!.createConnection)
: Text(AppLocalizations.of(context)!.editConnection),
actions: [
actions(),
const SizedBox(width: 8)
],
),
body: ListView(
children: form()
),
),
);
}
else {
return Dialog(
child: SizedBox(
width: 400,
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.clear_rounded)
),
const SizedBox(width: 8),
Text(
AppLocalizations.of(context)!.createConnection,
style: const TextStyle(
fontSize: 20
),
),
],
),
actions()
],
),
),
Expanded(
child: ListView(
children: form()
),
)
],
),
),
);
}
}
}

View file

@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
class FormTextField extends StatelessWidget {
final TextEditingController controller;
final String label;
final String? error;
final IconData icon;
final TextInputType? keyboardType;
final Function(String)? onChanged;
final bool? obscureText;
final String? hintText;
final String? helperText;
final bool isConnecting;
const FormTextField({
Key? key,
required this.label,
required this.controller,
this.error,
required this.icon,
this.keyboardType,
this.onChanged,
this.obscureText,
this.hintText,
this.helperText,
required this.isConnecting
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: TextFormField(
controller: controller,
onChanged: onChanged,
obscureText: obscureText ?? false,
enabled: !isConnecting,
decoration: InputDecoration(
prefixIcon: Icon(icon),
errorText: error,
hintText: hintText,
helperText: helperText,
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
labelText: label,
),
keyboardType: keyboardType,
),
);
}
}

View file

@ -1,766 +0,0 @@
// ignore_for_file: use_build_context_synchronously
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/functions/snackbar.dart';
import 'package:adguard_home_manager/constants/urls.dart';
import 'package:adguard_home_manager/functions/open_url.dart';
import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/providers/status_provider.dart';
import 'package:adguard_home_manager/functions/base64.dart';
import 'package:adguard_home_manager/services/http_requests.dart';
import 'package:adguard_home_manager/models/app_log.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
import 'package:adguard_home_manager/models/server.dart';
enum ConnectionType { http, https}
class AddServerModal extends StatefulWidget {
final Server? server;
final bool window;
final void Function(String version) onUnsupportedVersion;
const AddServerModal({
Key? key,
this.server,
required this.window,
required this.onUnsupportedVersion
}) : super(key: key);
@override
State<AddServerModal> createState() => _AddServerModalState();
}
class _AddServerModalState extends State<AddServerModal> {
final uuid = const Uuid();
final TextEditingController nameController = TextEditingController();
String? nameError;
ConnectionType connectionType = 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;
bool homeAssistant = false;
bool allDataValid = false;
bool isConnecting = false;
Widget sectionLabel(String label) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 24
),
child: Text(
label,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.primary
),
),
);
}
Widget textField({
required String label,
required TextEditingController controller,
String? error,
required IconData icon,
TextInputType? keyboardType,
Function(String)? onChanged,
bool? obscureText,
String? hintText,
String? helperText
}) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: TextFormField(
controller: controller,
onChanged: onChanged,
obscureText: obscureText ?? false,
decoration: InputDecoration(
prefixIcon: Icon(icon),
errorText: error,
hintText: hintText,
helperText: helperText,
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
labelText: label,
),
keyboardType: keyboardType,
),
);
}
void checkDataValid() {
if (
nameController.text != '' &&
ipDomainController.text != '' &&
ipDomainError == null &&
pathError == null &&
portError == null
) {
setState(() {
allDataValid = true;
});
}
else {
setState(() {
allDataValid = false;
});
}
}
void validatePort(String? value) {
if (value != null && value != '') {
if (int.tryParse(value) != null && int.parse(value) <= 65535) {
setState(() {
portError = null;
});
}
else {
setState(() {
portError = AppLocalizations.of(context)!.invalidPort;
});
}
}
else {
setState(() {
portError = null;
});
}
checkDataValid();
}
void validateSubroute(String? value) {
if (value != null && value != '') {
RegExp subrouteRegexp = RegExp(r'^\/\b([A-Za-z0-9_\-~/]*)[^\/|\.|\:]$');
if (subrouteRegexp.hasMatch(value) == true) {
setState(() {
pathError = null;
});
}
else {
setState(() {
pathError = AppLocalizations.of(context)!.invalidPath;
});
}
}
else {
setState(() {
pathError = null;
});
}
checkDataValid();
}
void validateAddress(String? value) {
if (value != null && value != '') {
RegExp ipAddress = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$');
RegExp domain = RegExp(r'^(([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+)|((\w|-)+)$');
if (ipAddress.hasMatch(value) == true || domain.hasMatch(value) == true) {
setState(() {
ipDomainError = null;
});
}
else {
setState(() {
ipDomainError = AppLocalizations.of(context)!.invalidIpDomain;
});
}
}
else {
setState(() {
ipDomainError = AppLocalizations.of(context)!.ipDomainNotEmpty;
});
}
checkDataValid();
}
@override
void initState() {
if (widget.server != null) {
nameController.text = widget.server!.name;
connectionType = widget.server!.connectionMethod == 'https' ? ConnectionType.https : ConnectionType.http;
ipDomainController.text = widget.server!.domain;
pathController.text = widget.server!.path ?? '';
portController.text = widget.server!.port != null ? widget.server!.port.toString() : "";
userController.text = widget.server!.user ?? "";
passwordController.text = widget.server!.password ?? "";
defaultServer = widget.server!.defaultServer;
homeAssistant = widget.server!.runningOnHa;
}
checkDataValid();
super.initState();
}
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context, listen: false);
final statusProvider = Provider.of<StatusProvider>(context, listen: false);
final appConfigProvider = Provider.of<AppConfigProvider>(context, listen: false);
final mediaQuery = MediaQuery.of(context);
void cancelConnecting() {
if (mounted) {
setState(() => isConnecting = false);
}
else {
isConnecting = false;
}
}
void connect() async {
Server serverObj = Server(
id: uuid.v4(),
name: nameController.text,
connectionMethod: connectionType.name,
domain: ipDomainController.text,
port: portController.text != '' ? int.parse(portController.text) : null,
user: userController.text != "" ? userController.text : null,
password: passwordController.text != "" ? passwordController.text : null,
defaultServer: defaultServer,
authToken: homeAssistant == true
? encodeBase64UserPass(userController.text, passwordController.text)
: null,
runningOnHa: homeAssistant
);
setState(() => isConnecting = true);
final result = homeAssistant == true
? await loginHA(serverObj)
: await login(serverObj);
if (!mounted) return;
if (result['result'] == 'success') {
if (serverObj.user != null && serverObj.password != null) {
serverObj.authToken = encodeBase64UserPass(serverObj.user!, serverObj.password!);
}
final serverCreated = await serversProvider.createServer(serverObj);
if (serverCreated == null) {
statusProvider.setServerStatusLoad(LoadStatus.loading);
final ApiClient apiClient = ApiClient(server: serverObj);
final serverStatus = await apiClient.getServerStatus();
if (!mounted) return;
if (serverStatus['result'] == 'success') {
statusProvider.setServerStatusData(
data: serverStatus['data']
);
serversProvider.setApiClient(apiClient);
statusProvider.setServerStatusLoad(LoadStatus.loaded);
if (serverStatus['data'].serverVersion.contains('a') || serverStatus['data'].serverVersion.contains('b')) {
Navigator.pop(context);
widget.onUnsupportedVersion(serverStatus['data'].serverVersion);
}
else {
Navigator.pop(context);
}
}
else {
appConfigProvider.addLog(serverStatus['log']);
statusProvider.setServerStatusLoad(LoadStatus.error);
Navigator.pop(context);
}
}
else {
setState(() => isConnecting = false);
appConfigProvider.addLog(
AppLog(
type: 'save_connection_db',
dateTime: DateTime.now(),
message: serverCreated.toString()
)
);
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.connectionNotCreated,
color: Colors.red
);
}
}
else if (result['result'] == 'invalid_username_password') {
cancelConnecting();
appConfigProvider.addLog(result['log']);
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.invalidUsernamePassword,
color: Colors.red
);
}
else if (result['result'] == 'many_attempts') {
cancelConnecting();
appConfigProvider.addLog(result['log']);
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.tooManyAttempts,
color: Colors.red
);
}
else if (result['result'] == 'no_connection') {
cancelConnecting();
appConfigProvider.addLog(result['log']);
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.cantReachServer,
color: Colors.red
);
}
else if (result['result'] == 'ssl_error') {
cancelConnecting();
appConfigProvider.addLog(result['log']);
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.sslError,
color: Colors.red
);
}
else if (result['result'] == 'server_error') {
cancelConnecting();
appConfigProvider.addLog(result['log']);
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.serverError,
color: Colors.red
);
}
else {
cancelConnecting();
appConfigProvider.addLog(result['log']);
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.unknownError,
color: Colors.red
);
}
}
void edit() async {
final Server serverObj = Server(
id: widget.server!.id,
name: nameController.text,
connectionMethod: connectionType.name,
domain: ipDomainController.text,
port: portController.text != '' ? int.parse(portController.text) : null,
user: userController.text != "" ? userController.text : null,
password: passwordController.text != "" ? passwordController.text : null,
defaultServer: defaultServer,
authToken: homeAssistant == true
? encodeBase64UserPass(userController.text, passwordController.text)
: null,
runningOnHa: homeAssistant
);
final result = homeAssistant == true
? await loginHA(serverObj)
: await login(serverObj);
if (!mounted) return;
if (result['result'] == 'success') {
if (serverObj.user != null && serverObj.password != null) {
serverObj.authToken = encodeBase64UserPass(serverObj.user!, serverObj.password!);
}
final serverSaved = await serversProvider.editServer(serverObj);
if (!mounted) return;
if (serverSaved == null) {
final ApiClient apiClient = ApiClient(server: serverObj);
final version = await apiClient.getServerVersion();
if (
version['result'] == 'success' &&
(version['data'].contains('a') || version['data'].contains('b')) // alpha or beta
) {
Navigator.pop(context);
widget.onUnsupportedVersion(version['data']);
}
else {
Navigator.pop(context);
}
}
else {
appConfigProvider.addLog(
AppLog(
type: 'edit_connection_db',
dateTime: DateTime.now(),
message: serverSaved.toString()
)
);
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.connectionNotCreated,
color: Colors.red
);
}
}
else if (result['result'] == 'invalid_username_password') {
appConfigProvider.addLog(result['log']);
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.invalidUsernamePassword,
color: Colors.red
);
}
else if (result['result'] == 'many_attempts') {
appConfigProvider.addLog(result['log']);
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.tooManyAttempts,
color: Colors.red
);
}
else if (result['result'] == 'no_connection') {
appConfigProvider.addLog(result['log']);
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.cantReachServer,
color: Colors.red
);
}
else if (result['result'] == 'ssl_error') {
appConfigProvider.addLog(result['log']);
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.sslError,
color: Colors.red
);
}
else if (result['result'] == 'server_error') {
appConfigProvider.addLog(result['log']);
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.serverError,
color: Colors.red
);
}
else {
appConfigProvider.addLog(result['log']);
showSnacbkar(
appConfigProvider: appConfigProvider,
label: AppLocalizations.of(context)!.unknownError,
color: Colors.red
);
}
}
List<Widget> form() {
return [
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
margin: const EdgeInsets.only(
top: 24,
left: 24,
right: 24
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.05),
borderRadius: BorderRadius.circular(30),
border: Border.all(
color: Theme.of(context).colorScheme.primary
)
),
child: Text(
"${connectionType.name}://${ipDomainController.text}${portController.text != '' ? ':${portController.text}' : ""}${pathController.text}",
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500
),
),
),
sectionLabel(AppLocalizations.of(context)!.general),
textField(
label: AppLocalizations.of(context)!.name,
controller: nameController,
icon: Icons.badge_rounded,
error: nameError,
onChanged: (value) {
if (value != '') {
setState(() => nameError = null);
}
else {
setState(() => nameError = AppLocalizations.of(context)!.nameNotEmpty);
}
checkDataValid();
}
),
sectionLabel(AppLocalizations.of(context)!.connection),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: SegmentedButton<ConnectionType>(
segments: const [
ButtonSegment(
value: ConnectionType.http,
label: Text("HTTP")
),
ButtonSegment(
value: ConnectionType.https,
label: Text("HTTPS")
),
],
selected: <ConnectionType>{connectionType},
onSelectionChanged: (value) => setState(() => connectionType = value.first),
),
),
const SizedBox(height: 30),
textField(
label: AppLocalizations.of(context)!.ipDomain,
controller: ipDomainController,
icon: Icons.link_rounded,
error: ipDomainError,
keyboardType: TextInputType.url,
onChanged: validateAddress
),
const SizedBox(height: 20),
textField(
label: AppLocalizations.of(context)!.path,
controller: pathController,
icon: Icons.route_rounded,
error: pathError,
onChanged: validateSubroute,
hintText: AppLocalizations.of(context)!.examplePath,
helperText: AppLocalizations.of(context)!.helperPath,
),
const SizedBox(height: 20),
textField(
label: AppLocalizations.of(context)!.port,
controller: portController,
icon: Icons.numbers_rounded,
error: portError,
keyboardType: TextInputType.number,
onChanged: validatePort
),
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,
obscureText: true
),
sectionLabel(AppLocalizations.of(context)!.other),
Material(
color: Colors.transparent,
child: InkWell(
onTap: widget.server == null
? () => setState(() => defaultServer = !defaultServer)
: null,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppLocalizations.of(context)!.defaultServer,
style: const TextStyle(
fontSize: 15,
),
),
Switch(
value: defaultServer,
onChanged: widget.server == null
? (value) => setState(() => defaultServer = value)
: null,
)
],
),
),
),
),
const SizedBox(height: 20),
Material(
color: Colors.transparent,
child: InkWell(
onTap: () => setState(() => homeAssistant = !homeAssistant),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppLocalizations.of(context)!.runningHomeAssistant,
style: const TextStyle(
fontSize: 15,
),
),
Switch(
value: homeAssistant,
onChanged: (value) => setState(() => homeAssistant = value),
)
],
),
),
),
),
const SizedBox(height: 20),
];
}
if (widget.window == true) {
return Dialog(
child: SizedBox(
width: 400,
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.clear_rounded)
),
const SizedBox(width: 8),
Text(
AppLocalizations.of(context)!.createConnection,
style: const TextStyle(
fontSize: 20
),
),
],
),
Row(
children: [
IconButton(
onPressed: () => openUrl(Urls.connectionInstructions),
icon: const Icon(Icons.help_outline_outlined)
),
IconButton(
tooltip: widget.server == null
? AppLocalizations.of(context)!.connect
: AppLocalizations.of(context)!.save,
onPressed: allDataValid == true
? widget.server == null
? () => connect()
: () => edit()
: null,
icon: Icon(
widget.server == null
? Icons.login_rounded
: Icons.save_rounded
)
),
],
),
],
),
),
Expanded(
child: ListView(
children: form()
),
)
],
),
),
);
}
else {
return Stack(
children: [
Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.createConnection),
actions: [
IconButton(
onPressed: () => openUrl(Urls.connectionInstructions),
icon: const Icon(Icons.help_outline_outlined)
),
IconButton(
tooltip: widget.server == null
? AppLocalizations.of(context)!.connect
: AppLocalizations.of(context)!.save,
onPressed: allDataValid == true
? widget.server == null
? () => connect()
: () => edit()
: null,
icon: Icon(
widget.server == null
? Icons.login_rounded
: Icons.save_rounded
)
),
const SizedBox(width: 10)
],
toolbarHeight: 70,
),
body: ListView(
children: form(),
)
),
AnimatedOpacity(
opacity: isConnecting == true ? 1 : 0,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
child: IgnorePointer(
ignoring: isConnecting == true ? false : true,
child: Scaffold(
backgroundColor: Colors.transparent,
body: Container(
width: mediaQuery.size.width,
height: mediaQuery.size.height,
color: const Color.fromRGBO(0, 0, 0, 0.7),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(
color: Colors.white,
),
const SizedBox(height: 30),
Text(
AppLocalizations.of(context)!.connecting,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
fontSize: 26
),
)
],
),
),
),
),
)
],
);
}
}
}

View file

@ -1,17 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class OptionBox extends StatelessWidget { class OptionBox extends StatelessWidget {
final Widget child;
final dynamic optionsValue; final dynamic optionsValue;
final dynamic itemValue; final dynamic itemValue;
final void Function(dynamic) onTap; final void Function(dynamic) onTap;
final String label;
const OptionBox({ const OptionBox({
Key? key, Key? key,
required this.child,
required this.optionsValue, required this.optionsValue,
required this.itemValue, required this.itemValue,
required this.onTap, required this.onTap,
required this.label,
}) : super(key: key); }) : super(key: key);
@override @override
@ -25,19 +25,27 @@ class OptionBox extends StatelessWidget {
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut, curve: Curves.easeInOut,
padding: const EdgeInsets.all(15), padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50), borderRadius: BorderRadius.circular(50),
border: Border.all(
color: optionsValue == itemValue color: optionsValue == itemValue
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant : Theme.of(context).colorScheme.primaryContainer,
), ),
child: AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 250),
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
color: optionsValue == itemValue color: optionsValue == itemValue
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.onInverseSurface
: Colors.transparent, : Theme.of(context).colorScheme.onSurface
),
child: Text(
label,
textAlign: TextAlign.center,
),
), ),
child: child,
), ),
), ),
); );

View file

@ -24,7 +24,7 @@ class ProcessDialog extends StatelessWidget {
children: [ children: [
const CircularProgressIndicator(), const CircularProgressIndicator(),
const SizedBox(width: 40), const SizedBox(width: 40),
Expanded( Flexible(
child: Text( child: Text(
message, message,
style: TextStyle( style: TextStyle(

View file

@ -5,8 +5,7 @@ import 'package:expandable/expandable.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/widgets/version_warning_modal.dart'; import 'package:adguard_home_manager/widgets/add_server/add_server_functions.dart';
import 'package:adguard_home_manager/widgets/add_server_modal.dart';
import 'package:adguard_home_manager/widgets/servers_list/delete_modal.dart'; import 'package:adguard_home_manager/widgets/servers_list/delete_modal.dart';
import 'package:adguard_home_manager/classes/process_modal.dart'; import 'package:adguard_home_manager/classes/process_modal.dart';
@ -88,41 +87,9 @@ class _ServersListItemState extends State<ServersListItem> with SingleTickerProv
}); });
} }
void openAddServerBottomSheet({Server? server}) async { void openServerModal({Server? server}) async {
await Future.delayed(const Duration(seconds: 0), (() => { await Future.delayed(const Duration(seconds: 0), (() => {
if (width > 700) { openServerFormModal(context: context, width: width, server: server)
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AddServerModal(
server: server,
window: true,
onUnsupportedVersion: (version) => showDialog(
context: context,
builder: (ctx) => VersionWarningModal(
version: version
),
barrierDismissible: false
),
),
)
}
else {
Navigator.push(context, MaterialPageRoute(
fullscreenDialog: true,
builder: (BuildContext context) => AddServerModal(
server: server,
window: false,
onUnsupportedVersion: (version) => showDialog(
context: context,
builder: (ctx) => VersionWarningModal(
version: version
),
barrierDismissible: false
),
)
))
}
})); }));
} }
@ -326,7 +293,7 @@ class _ServersListItemState extends State<ServersListItem> with SingleTickerProv
) )
), ),
PopupMenuItem( PopupMenuItem(
onTap: (() => openAddServerBottomSheet(server: server)), onTap: (() => openServerModal(server: server)),
child: Row( child: Row(
children: [ children: [
const Icon(Icons.edit), const Icon(Icons.edit),

View file

@ -4,8 +4,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/widgets/version_warning_modal.dart'; import 'package:adguard_home_manager/widgets/add_server/add_server_functions.dart';
import 'package:adguard_home_manager/widgets/add_server_modal.dart';
import 'package:adguard_home_manager/widgets/servers_list/delete_modal.dart'; import 'package:adguard_home_manager/widgets/servers_list/delete_modal.dart';
import 'package:adguard_home_manager/classes/process_modal.dart'; import 'package:adguard_home_manager/classes/process_modal.dart';
@ -57,41 +56,9 @@ class _ServersTileItemState extends State<ServersTileItem> with SingleTickerProv
}); });
} }
void openAddServerBottomSheet({Server? server}) async { void openServerModal({Server? server}) async {
await Future.delayed(const Duration(seconds: 0), (() => { await Future.delayed(const Duration(seconds: 0), (() => {
if (width > 700) { openServerFormModal(context: context, width: width, server: server)
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AddServerModal(
server: server,
window: true,
onUnsupportedVersion: (version) => showDialog(
context: context,
builder: (ctx) => VersionWarningModal(
version: version
),
barrierDismissible: false
),
),
)
}
else {
Navigator.push(context, MaterialPageRoute(
fullscreenDialog: true,
builder: (BuildContext context) => AddServerModal(
server: server,
window: false,
onUnsupportedVersion: (version) => showDialog(
context: context,
builder: (ctx) => VersionWarningModal(
version: version
),
barrierDismissible: false
),
)
))
}
})); }));
} }
@ -291,7 +258,7 @@ class _ServersTileItemState extends State<ServersTileItem> with SingleTickerProv
) )
), ),
PopupMenuItem( PopupMenuItem(
onTap: (() => openAddServerBottomSheet(server: server)), onTap: (() => openServerModal(server: server)),
child: Row( child: Row(
children: [ children: [
const Icon(Icons.edit), const Icon(Icons.edit),