Refactor clients

This commit is contained in:
Juan Gilsanz Polo 2023-10-07 23:03:30 +02:00
parent 36bd7acfed
commit adaa75e717
22 changed files with 1009 additions and 851 deletions

View file

@ -0,0 +1,166 @@
import 'package:flutter/material.dart';
import 'package:contextmenu/contextmenu.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/widgets/custom_list_tile.dart';
import 'package:adguard_home_manager/widgets/options_modal.dart';
import 'package:adguard_home_manager/models/menu_option.dart';
import 'package:adguard_home_manager/functions/copy_clipboard.dart';
import 'package:adguard_home_manager/models/clients.dart';
class ActiveClientTile extends StatelessWidget {
final AutoClient client;
final void Function(AutoClient) onTap;
final bool splitView;
final AutoClient? selectedClient;
const ActiveClientTile({
Key? key,
required this.client,
required this.onTap,
required this.splitView,
this.selectedClient
}) : super(key: key);
@override
Widget build(BuildContext context) {
void openOptionsModal() {
showDialog(
context: context,
builder: (context) => OptionsModal(
options: [
MenuOption(
title: AppLocalizations.of(context)!.copyClipboard,
icon: Icons.copy_rounded,
action: () {
copyToClipboard(
value: client.name != ''
? client.name!
: client.ip,
successMessage: AppLocalizations.of(context)!.copiedClipboard,
);
},
)
]
),
);
}
if (splitView == true) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: ContextMenuArea(
builder: (context) => [
CustomListTile(
title: AppLocalizations.of(context)!.copyClipboard,
icon: Icons.copy_rounded,
onTap: () {
copyToClipboard(
value: client.name != ''
? client.name!
: client.ip,
successMessage: AppLocalizations.of(context)!.copiedClipboard,
);
Navigator.pop(context);
},
)
],
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(28),
child: InkWell(
borderRadius: BorderRadius.circular(28),
onTap: () => onTap(client),
onLongPress: () {
Navigator.pop(context);
openOptionsModal();
},
child: Container(
width: double.maxFinite,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
color: client == selectedClient
? Theme.of(context).colorScheme.primaryContainer
: null
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
client.name != ''
? client.name!
: client.ip,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface,
),
),
if (client.name != '') Text(client.ip)
],
),
)
],
),
),
Text(
client.source,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface
),
),
],
)
),
),
),
),
);
}
else {
return ContextMenuArea(
builder: (context) => [
CustomListTile(
title: AppLocalizations.of(context)!.copyClipboard,
icon: Icons.copy_rounded,
onTap: () {
copyToClipboard(
value: client.name != ''
? client.name!
: client.ip,
successMessage: AppLocalizations.of(context)!.copiedClipboard,
);
Navigator.pop(context);
},
)
],
child: CustomListTile(
title: client.name != ''
? client.name!
: client.ip,
subtitle: client.name != ''
? client.ip
: null,
trailing: Text(
client.source,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface
),
),
onTap: () => onTap(client),
onLongPress: openOptionsModal,
),
);
}
}
}

View file

@ -0,0 +1,291 @@
import 'package:flutter/material.dart';
import 'package:contextmenu/contextmenu.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.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/functions/copy_clipboard.dart';
import 'package:adguard_home_manager/models/clients.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
class AddedClientTile extends StatelessWidget {
final Client client;
final void Function(Client) onTap;
final void Function(Client) onLongPress;
final void Function(Client) onEdit;
final void Function(Client) onDelete;
final Client? selectedClient;
final bool? splitView;
final String serverVersion;
const AddedClientTile({
Key? key,
required this.client,
required this.onTap,
required this.onLongPress,
required this.onEdit,
required this.onDelete,
this.selectedClient,
required this.splitView,
required this.serverVersion
}) : super(key: key);
@override
Widget build(BuildContext context) {
final appConfigProvider = Provider.of<AppConfigProvider>(context);
if (splitView == true) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(28),
child: ContextMenuArea(
builder: (context) => [
CustomListTile(
title: AppLocalizations.of(context)!.edit,
icon: Icons.edit_rounded,
onTap: () {
Navigator.pop(context);
onEdit(client);
}
),
CustomListTile(
title: AppLocalizations.of(context)!.delete,
icon: Icons.delete_rounded,
onTap: () {
Navigator.pop(context);
onDelete(client);
}
),
CustomListTile(
title: AppLocalizations.of(context)!.copyClipboard,
icon: Icons.copy_rounded,
onTap: () {
copyToClipboard(
value: client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''),
successMessage: AppLocalizations.of(context)!.copiedClipboard,
);
Navigator.pop(context);
}
),
],
child: InkWell(
borderRadius: BorderRadius.circular(28),
onTap: () => onTap(client),
onLongPress: () => onLongPress(client),
child: Container(
width: double.maxFinite,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
color: client == selectedClient
? Theme.of(context).colorScheme.primaryContainer
: null
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.filter_list_rounded,
size: 19,
color: client.filteringEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
),
const SizedBox(width: 10),
Icon(
Icons.vpn_lock_rounded,
size: 18,
color: client.safebrowsingEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
),
const SizedBox(width: 10),
Icon(
Icons.block,
size: 18,
color: client.parentalEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
),
const SizedBox(width: 10),
Icon(
Icons.search_rounded,
size: 19,
color: serverVersionIsAhead(
currentVersion: serverVersion,
referenceVersion: 'v0.107.28',
referenceVersionBeta: 'v0.108.0-b.33'
) == true
? client.safeSearch != null && client.safeSearch!.enabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red
: client.safesearchEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
)
],
)
],
),
)
],
),
),
],
)
),
),
),
),
);
}
else {
return ContextMenuArea(
builder: (context) => [
CustomListTile(
title: AppLocalizations.of(context)!.seeDetails,
icon: Icons.file_open_rounded,
onTap: () {
Navigator.pop(context);
onEdit(client);
}
),
CustomListTile(
title: AppLocalizations.of(context)!.copyClipboard,
icon: Icons.copy_rounded,
onTap: () {
copyToClipboard(
value: client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''),
successMessage: AppLocalizations.of(context)!.copiedClipboard,
);
Navigator.pop(context);
}
),
],
child: CustomListTile(
onLongPress: () => onLongPress(client),
onTap: () => onTap(client),
title: client.name,
subtitleWidget: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''),
style: TextStyle(
color: Theme.of(context).listTileTheme.textColor
),
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.filter_list_rounded,
size: 19,
color: client.filteringEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
),
const SizedBox(width: 10),
Icon(
Icons.vpn_lock_rounded,
size: 18,
color: client.safebrowsingEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
),
const SizedBox(width: 10),
Icon(
Icons.block,
size: 18,
color: client.parentalEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
),
const SizedBox(width: 10),
Icon(
Icons.search_rounded,
size: 19,
color: serverVersionIsAhead(
currentVersion: serverVersion,
referenceVersion: 'v0.107.28',
referenceVersionBeta: 'v0.108.0-b.33'
) == true
? client.safeSearch != null && client.safeSearch!.enabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red
: client.safesearchEnabled == true
? appConfigProvider.useThemeColorForStatus == true
? Theme.of(context).colorScheme.primary
: Colors.green
: appConfigProvider.useThemeColorForStatus == true
? Colors.grey
: Colors.red,
)
],
)
],
),
),
);
}
}
}

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 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 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: widget.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.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: [
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)
)
],
),
),
);
}
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: actions(),
),
body: content(true)
);
}
}
}

View file

@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.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/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;
}
}

View file

@ -0,0 +1,103 @@
import 'package:adguard_home_manager/screens/clients/client/client_screen_functions.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/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,268 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:async/async.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/logs/log_tile.dart';
import 'package:adguard_home_manager/screens/logs/log_details_screen.dart';
import 'package:adguard_home_manager/models/logs.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
class LogsListClient extends StatefulWidget {
final String ip;
final String? name;
final ServersProvider serversProvider;
final AppConfigProvider appConfigProvider;
const LogsListClient({
Key? key,
required this.ip,
this.name,
required this.serversProvider,
required this.appConfigProvider
}) : super(key: key);
@override
State<LogsListClient> createState() => _LogsListClientState();
}
class _LogsListClientState extends State<LogsListClient> {
late ScrollController scrollController;
bool isLoadingMore = false;
int logsQuantity = 100;
int offset = 0;
int loadStatus = 0;
LogsData? logsData;
String previousIp = "";
bool showDivider = true;
CancelableOperation? cancelableRequest;
Future fetchLogs({
int? inOffset,
bool? loadingMore,
String? responseStatus,
String? searchText,
}) async {
final serversProvider = Provider.of<ServersProvider>(context, listen: false);
int offst = inOffset ?? offset;
if (loadingMore != null && loadingMore == true) {
setState(() => isLoadingMore = true);
}
if (cancelableRequest != null) cancelableRequest!.cancel();
cancelableRequest = CancelableOperation.fromFuture(
serversProvider.apiClient!.getLogs(
count: logsQuantity,
offset: offst,
search: '"${widget.ip}"'
)
);
final result = await cancelableRequest?.value;
if (result != null) {
if (loadingMore != null && loadingMore == true && mounted) {
setState(() => isLoadingMore = false);
}
if (mounted) {
if (result['result'] == 'success') {
setState(() => offset = inOffset != null ? inOffset+logsQuantity : offset+logsQuantity);
if (loadingMore != null && loadingMore == true && logsData != null) {
LogsData newLogsData = result['data'];
newLogsData.data = [...logsData!.data, ...result['data'].data];
setState(() => logsData = newLogsData);
}
else {
LogsData newLogsData = result['data'];
setState(() => logsData = newLogsData);
}
setState(() => loadStatus = 1);
}
else {
setState(() => loadStatus = 2);
widget.appConfigProvider.addLog(result['log']);
}
}
}
}
void scrollListener() {
if (scrollController.position.extentAfter < 500 && isLoadingMore == false) {
fetchLogs(loadingMore: true);
}
if (scrollController.position.pixels > 0) {
setState(() => showDivider = false);
}
else {
setState(() => showDivider = true);
}
}
@override
void initState() {
scrollController = ScrollController()..addListener(scrollListener);
fetchLogs(inOffset: 0);
setState(() => previousIp = widget.ip);
super.initState();
}
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
if (widget.ip != previousIp) {
setState(() => loadStatus = 0);
fetchLogs(inOffset: 0);
setState(() => previousIp = widget.ip);
}
Widget status() {
switch (loadStatus) {
case 0:
return SizedBox(
width: double.maxFinite,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 30),
Text(
AppLocalizations.of(context)!.loadingLogs,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 22,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
],
),
);
case 1:
if (logsData!.data.isNotEmpty) {
return RefreshIndicator(
onRefresh: fetchLogs,
child: ListView.builder(
controller: scrollController,
padding: const EdgeInsets.only(top: 0),
itemCount: isLoadingMore == true
? logsData!.data.length+1
: logsData!.data.length,
itemBuilder: (context, index) {
if (isLoadingMore == true && index == logsData!.data.length) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Center(
child: CircularProgressIndicator(),
),
);
}
else {
return LogTile(
log: logsData!.data[index],
index: index,
length: logsData!.data.length,
useAlwaysNormalTile: true,
onLogTap: (log) => {
if (width > 700) {
showDialog(
context: context,
builder: (context) => LogDetailsScreen(
log: log,
dialog: true
)
)
}
else {
Navigator.push(context, MaterialPageRoute(
builder: (context) => LogDetailsScreen(
log: log,
dialog: false
)
))
}
}
);
}
}
),
);
}
else {
return Center(
child: Text(
AppLocalizations.of(context)!.noLogsDisplay,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 22,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
);
}
case 2:
return SizedBox(
width: double.maxFinite,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.error,
color: Colors.red,
size: 50,
),
const SizedBox(height: 30),
Text(
AppLocalizations.of(context)!.logsNotLoaded,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 22,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
],
),
);
default:
return const SizedBox();
}
}
return Scaffold(
appBar: AppBar(
title: Text(widget.name != null && widget.name != '' ? widget.name! : widget.ip),
centerTitle: true,
actions: [
if (!(Platform.isAndroid || Platform.isIOS)) ...[
IconButton(
onPressed: fetchLogs,
icon: const Icon(Icons.refresh_rounded),
tooltip: AppLocalizations.of(context)!.refresh,
),
const SizedBox(width: 8)
]
],
),
body: status(),
);
}
}

View file

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class RemoveClientModal extends StatelessWidget {
final void Function() onConfirm;
const RemoveClientModal({
Key? key,
required this.onConfirm
}) : super(key: key);
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Column(
children: [
Icon(
Icons.delete_rounded,
size: 24,
color: Theme.of(context).listTileTheme.iconColor
),
const SizedBox(height: 16),
Text(
AppLocalizations.of(context)!.removeClient,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface
),
)
],
),
content: Text(
AppLocalizations.of(context)!.removeClientMessage,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(AppLocalizations.of(context)!.cancel)
),
TextButton(
onPressed: () {
onConfirm();
Navigator.pop(context);
},
child: Text(AppLocalizations.of(context)!.confirm)
),
],
);
}
}

View file

@ -0,0 +1,201 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/widgets/custom_checkbox_list_tile.dart';
import 'package:adguard_home_manager/models/safe_search.dart';
class SafeSearchModal extends StatefulWidget {
final SafeSearch safeSearch;
final bool disabled;
final void Function(SafeSearch) onConfirm;
const SafeSearchModal({
Key? key,
required this.safeSearch,
required this.disabled,
required this.onConfirm
}) : super(key: key);
@override
State<SafeSearchModal> createState() => _SafeSearchModalState();
}
class _SafeSearchModalState extends State<SafeSearchModal> {
bool generalEnabled = false;
bool bingEnabled = false;
bool duckduckgoEnabled = false;
bool googleEnabled = false;
bool pixabayEnabled = false;
bool yandexEnabled = false;
bool youtubeEnabled = false;
@override
void initState() {
generalEnabled = widget.safeSearch.enabled;
bingEnabled = widget.safeSearch.bing;
duckduckgoEnabled = widget.safeSearch.duckduckgo;
googleEnabled = widget.safeSearch.google;
pixabayEnabled = widget.safeSearch.pixabay;
yandexEnabled = widget.safeSearch.yandex;
youtubeEnabled = widget.safeSearch.youtube;
super.initState();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 16),
title: Column(
children: [
Icon(
Icons.search_rounded,
size: 24,
color: Theme.of(context).listTileTheme.iconColor
),
const SizedBox(height: 16),
Text(
AppLocalizations.of(context)!.safeSearch,
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface
),
)
],
),
content: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 400
),
child: Wrap(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Material(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(28),
child: InkWell(
onTap: widget.disabled == true
? null
: () => setState(() => generalEnabled = !generalEnabled),
borderRadius: BorderRadius.circular(28),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 5
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppLocalizations.of(context)!.enable,
style: TextStyle(
fontSize: 16,
color: widget.disabled == true
? Colors.grey
: Theme.of(context).colorScheme.onSurface
),
),
Switch(
value: generalEnabled,
onChanged: widget.disabled == true
? null
: (value) => setState(() => generalEnabled = value),
)
],
),
),
),
),
),
const SizedBox(height: 4, width: double.maxFinite),
CustomCheckboxListTile(
value: bingEnabled,
onChanged: (value) => setState(() => bingEnabled = value),
title: "Bing",
disabled: widget.disabled || !generalEnabled,
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 4
),
),
CustomCheckboxListTile(
value: duckduckgoEnabled,
onChanged: (value) => setState(() => duckduckgoEnabled = value),
title: "DuckDuckGo",
disabled: widget.disabled || !generalEnabled,
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 4
),
),
CustomCheckboxListTile(
value: googleEnabled,
onChanged: (value) => setState(() => googleEnabled = value),
title: "Google",
disabled: widget.disabled || !generalEnabled,
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 4
),
),
CustomCheckboxListTile(
value: pixabayEnabled,
onChanged: (value) => setState(() => pixabayEnabled = value),
title: "Pixabay",
disabled: widget.disabled || !generalEnabled,
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 4
),
),
CustomCheckboxListTile(
value: yandexEnabled,
onChanged: (value) => setState(() => yandexEnabled = value),
title: "Yandex",
disabled: widget.disabled || !generalEnabled,
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 4
),
),
CustomCheckboxListTile(
value: youtubeEnabled,
onChanged: (value) => setState(() => youtubeEnabled = value),
title: "YouTube",
disabled: widget.disabled || !generalEnabled,
padding: const EdgeInsets.symmetric(
horizontal: 36,
vertical: 4
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(AppLocalizations.of(context)!.cancel)
),
TextButton(
onPressed: widget.disabled == false ? () {
widget.onConfirm(
SafeSearch(
enabled: generalEnabled,
bing: bingEnabled,
duckduckgo: duckduckgoEnabled,
google: googleEnabled,
pixabay: pixabayEnabled,
yandex: yandexEnabled,
youtube: youtubeEnabled
)
);
Navigator.pop(context);
} : null,
child: Text(AppLocalizations.of(context)!.confirm)
),
],
);
}
}

View file

@ -0,0 +1,188 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/constants/enums.dart';
import 'package:adguard_home_manager/providers/filtering_provider.dart';
class ServicesModal extends StatefulWidget {
final List<String> blockedServices;
final void Function(List<String>) onConfirm;
const ServicesModal({
Key? key,
required this.blockedServices,
required this.onConfirm,
}) : super(key: key);
@override
State<ServicesModal> createState() => _ServicesModalStateWidget();
}
class _ServicesModalStateWidget extends State<ServicesModal> {
List<String> blockedServices = [];
@override
void initState() {
final filteringProvider = Provider.of<FilteringProvider>(context, listen: false);
if (filteringProvider.blockedServicesLoadStatus != LoadStatus.loaded) {
filteringProvider.getBlockedServices();
}
blockedServices = widget.blockedServices;
super.initState();
}
void checkUncheckService(bool value, String service) {
if (value == true) {
setState(() {
blockedServices.add(service);
});
}
else if (value == false) {
setState(() {
blockedServices = blockedServices.where((s) => s != service).toList();
});
}
}
@override
Widget build(BuildContext context) {
final filteringProvider = Provider.of<FilteringProvider>(context);
Widget content() {
switch (filteringProvider.blockedServicesLoadStatus) {
case LoadStatus.loading:
return Padding(
padding: const EdgeInsets.all(24),
child: SizedBox(
width: double.maxFinite,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(
AppLocalizations.of(context)!.loadingBlockedServicesList,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
],
),
),
);
case LoadStatus.loaded:
return SizedBox(
width: double.minPositive,
height: MediaQuery.of(context).size.height*0.5,
child: ListView.builder(
shrinkWrap: true,
itemCount: filteringProvider.blockedServices!.services.length,
itemBuilder: (context, index) => CheckboxListTile(
title: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
filteringProvider.blockedServices!.services[index].name,
style: TextStyle(
fontWeight: FontWeight.normal,
color: Theme.of(context).colorScheme.onSurface
),
),
),
value: blockedServices.contains(filteringProvider.blockedServices!.services[index].id),
checkboxShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)
),
onChanged: (value) => checkUncheckService(value!, filteringProvider.blockedServices!.services[index].id)
)
),
);
case LoadStatus.error:
return Padding(
padding: const EdgeInsets.all(24),
child: SizedBox(
width: double.maxFinite,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.error,
color: Colors.red,
size: 26,
),
const SizedBox(height: 20),
Text(
AppLocalizations.of(context)!.blockedServicesListNotLoaded,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
],
),
),
);
default:
return const SizedBox();
}
}
return AlertDialog(
scrollable: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 0,
vertical: 16
),
title: Column(
children: [
Icon(
Icons.public,
color: Theme.of(context).listTileTheme.iconColor
),
const SizedBox(height: 16),
Text(
AppLocalizations.of(context)!.services,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
)
],
),
content: content(),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(AppLocalizations.of(context)!.cancel)
),
TextButton(
onPressed: blockedServices.isNotEmpty
? () {
widget.onConfirm(blockedServices);
Navigator.pop(context);
}
: null,
child: Text(
AppLocalizations.of(context)!.confirm,
style: TextStyle(
color: blockedServices.isNotEmpty
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface.withOpacity(0.38)
),
)
),
],
);
}
}

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,145 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class TagsModal extends StatefulWidget {
final List<String> selectedTags;
final List<String> tags;
final void Function(List<String>) onConfirm;
const TagsModal({
Key? key,
required this.selectedTags,
required this.tags,
required this.onConfirm,
}) : super(key: key);
@override
State<TagsModal> createState() => _TagsModalState();
}
class _TagsModalState extends State<TagsModal> {
List<String> selectedTags = [];
@override
void initState() {
selectedTags = widget.selectedTags;
super.initState();
}
@override
Widget build(BuildContext context) {
void checkUncheckTag(bool value, String tag) {
if (value == true) {
setState(() {
selectedTags.add(tag);
});
}
else if (value == false) {
setState(() {
selectedTags = selectedTags.where((s) => s != tag).toList();
});
}
}
return AlertDialog(
scrollable: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 0,
vertical: 16
),
title: Column(
children: [
Icon(
Icons.label_rounded,
color: Theme.of(context).listTileTheme.iconColor
),
const SizedBox(height: 16),
Text(
AppLocalizations.of(context)!.tags,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface
),
)
],
),
content: SizedBox(
width: double.minPositive,
height: MediaQuery.of(context).size.height*0.5,
child: ListView.builder(
shrinkWrap: true,
itemCount: widget.tags.length,
itemBuilder: (context, index) => Material(
color: Colors.transparent,
child: InkWell(
onTap: () => checkUncheckTag(!selectedTags.contains(widget.tags[index]), widget.tags[index]),
child: Container(
padding: const EdgeInsets.only(
left: 24,
top: 8,
bottom: 8,
right: 12
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.tags[index],
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurface,
),
),
Checkbox(
value: selectedTags.contains(widget.tags[index]),
onChanged: (value) => checkUncheckTag(value!, widget.tags[index]),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)
),
)
],
),
),
),
),
// itemBuilder: (context, index) => CheckboxListTile(
// title: Text(
// widget.tags[index],
// style: TextStyle(
// fontWeight: FontWeight.normal,
// color: Theme.of(context).colorScheme.onSurface
// ),
// ),
// value: selectedTags.contains(widget.tags[index]),
// checkboxShape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(5)
// ),
// onChanged: (value) => checkUncheckTag(value!, widget.tags[index])
// )
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(AppLocalizations.of(context)!.cancel)
),
TextButton(
onPressed: selectedTags.isNotEmpty
? () {
widget.onConfirm(selectedTags);
Navigator.pop(context);
}
: null,
child: Text(
AppLocalizations.of(context)!.confirm,
style: TextStyle(
color: selectedTags.isNotEmpty
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface.withOpacity(0.38)
),
)
),
],
);
}
}

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,
),
),
],
),
),
],
);
}
}