Added new combined chart on home

This commit is contained in:
Juan Gilsanz Polo 2023-05-20 22:57:38 +02:00
parent 8ca59d26c7
commit cf5af1061f
10 changed files with 506 additions and 25 deletions

View file

@ -280,7 +280,7 @@
"accessSettingsDescription": "Configure access rules for the server",
"loadingClients": "Loading clients...",
"clientsNotLoaded": "Clients couldn't be loaded.",
"noAllowedClients": "No hay clientes permitidos",
"noAllowedClients": "No allowed clients",
"allowedClientsDescription": "If this list has entries, AdGuard Home will accept requests only from these clients.",
"blockedClientsDescription": "If this list has entries, AdGuard Home will drop requests from these clients. This field is ignored if there are entries in Allowed clients.",
"disallowedDomainsDescription": "AdGuard Home drops DNS queries matching these domains, and these queries don't even appear in the query log.",
@ -625,5 +625,8 @@
"usingLatestVersion": "You are using the latest version",
"ipLogs": "IP on logs",
"ipLogsDescription": "Show always IP address on logs instead of client name",
"application": "Application"
"application": "Application",
"combinedChart": "Combined chart",
"combinedChartDescription": "Combine all charts into one",
"statistics": "Statistics"
}

View file

@ -625,5 +625,8 @@
"usingLatestVersion": "Estás usando la última versión",
"ipLogs": "IP en registros",
"ipLogsDescription": "Mostrar siempre dirección IP en vez del nombre del cliente",
"application": "Aplicación"
"application": "Aplicación",
"combinedChart": "Gráfico combinado",
"combinedChartDescription": "Combina todos los gráficos en uno solo",
"statistics": "Estadísticas"
}

View file

@ -41,6 +41,8 @@ class AppConfigProvider with ChangeNotifier {
int _showIpLogs = 0;
int _combinedChartHome = 0;
String? _doNotRememberVersion;
GitHubRelease? _appUpdatesAvailable;
@ -129,6 +131,10 @@ class AppConfigProvider with ChangeNotifier {
return _showIpLogs == 1 ? true : false;
}
bool get combinedChartHome {
return _combinedChartHome == 1 ? true : false;
}
String? get doNotRememberVersion {
return _doNotRememberVersion;
}
@ -315,6 +321,22 @@ class AppConfigProvider with ChangeNotifier {
}
}
Future<bool> setCombinedChartHome(bool value) async {
final updated = await updateConfigQuery(
db: _dbInstance!,
column: 'combinedChart',
value: value == true ? 1 : 0
);
if (updated == true) {
_combinedChartHome = value == true ? 1 : 0;
notifyListeners();
return true;
}
else {
return false;
}
}
Future<bool> setStaticColor(int value) async {
final updated = await updateConfigQuery(
db: _dbInstance!,
@ -349,6 +371,7 @@ class AppConfigProvider with ChangeNotifier {
_useThemeColorForStatus = dbData['useThemeColorForStatus'] != null ? convertFromIntToBool(dbData['useThemeColorForStatus'])! : false;
_showTimeLogs = dbData['showTimeLogs'];
_doNotRememberVersion = dbData['doNotRememberVersion'];
_combinedChartHome = dbData['combinedChart'];
_dbInstance = dbInstance;
notifyListeners();

View file

@ -37,7 +37,7 @@ class HomeChart extends StatelessWidget {
return Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
Padding(
@ -115,13 +115,13 @@ class HomeChart extends StatelessWidget {
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Divider(
thickness: 1,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2),
),
),
const SizedBox(height: 20),
const SizedBox(height: 16),
],
);
}

View file

@ -0,0 +1,285 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/widgets/combined_line_chart.dart';
import 'package:adguard_home_manager/functions/number_format.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
class CombinedChartData {
final CombinedChartItem totalQueries;
final CombinedChartItem? blockedFilters;
final CombinedChartItem? replacedSafeBrowsing;
final CombinedChartItem? replacedParental;
const CombinedChartData({
required this.totalQueries,
this.blockedFilters,
this.replacedSafeBrowsing,
this.replacedParental
});
}
class CombinedChartItem {
final String label;
final Color color;
final List<int> data;
const CombinedChartItem({
required this.label,
required this.color,
required this.data
});
}
class CombinedHomeChart extends StatelessWidget {
const CombinedHomeChart({Key? key}) : super(key: key);
List<int>? removeZero(List<int> list) {
final removed = list.where((i) => i > 0);
if (removed.isNotEmpty) {
return list;
}
else {
return null;
}
}
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
final width = MediaQuery.of(context).size.width;
if (serversProvider.serverStatus.data != null) {
final data = CombinedChartData(
totalQueries: CombinedChartItem(
label: AppLocalizations.of(context)!.dnsQueries,
color: Colors.blue,
data: serversProvider.serverStatus.data!.stats.dnsQueries
),
blockedFilters: appConfigProvider.hideZeroValues == true
? removeZero(serversProvider.serverStatus.data!.stats.blockedFiltering) != null
? CombinedChartItem(
label: AppLocalizations.of(context)!.blockedFilters,
color: Colors.red,
data: serversProvider.serverStatus.data!.stats.blockedFiltering
)
: null
: CombinedChartItem(
label: AppLocalizations.of(context)!.blockedFilters,
color: Colors.red,
data: serversProvider.serverStatus.data!.stats.blockedFiltering
) ,
replacedSafeBrowsing: appConfigProvider.hideZeroValues == true
? removeZero(serversProvider.serverStatus.data!.stats.replacedSafebrowsing) != null
? CombinedChartItem(
label: AppLocalizations.of(context)!.malwarePhisingBlocked,
color: Colors.green,
data: serversProvider.serverStatus.data!.stats.replacedSafebrowsing
)
: null
: CombinedChartItem(
label: AppLocalizations.of(context)!.malwarePhisingBlocked,
color: Colors.green,
data: serversProvider.serverStatus.data!.stats.replacedSafebrowsing
) ,
replacedParental: appConfigProvider.hideZeroValues == true
? removeZero(serversProvider.serverStatus.data!.stats.replacedParental) != null
? CombinedChartItem(
label: AppLocalizations.of(context)!.blockedAdultWebsites,
color: Colors.orange,
data: serversProvider.serverStatus.data!.stats.replacedParental
)
: null
: CombinedChartItem(
label: AppLocalizations.of(context)!.blockedAdultWebsites,
color: Colors.orange,
data: serversProvider.serverStatus.data!.stats.replacedParental
) ,
);
Widget legend({
required String label,
required Color color,
required String primaryValue,
String? secondaryValue
}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Text(
label,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: color
),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
primaryValue,
style: TextStyle(
color: color,
fontSize: 16,
fontWeight: FontWeight.w500
),
),
if (secondaryValue != null) Text(
secondaryValue,
style: TextStyle(
fontSize: 10,
color: color
),
)
],
)
],
);
}
if (width > 900) {
return Column(
children: [
Text(
AppLocalizations.of(context)!.statistics,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
flex: 3,
child: SizedBox(
height: 300,
width: double.maxFinite,
child: CustomCombinedLineChart(
inputData: data,
),
),
),
const SizedBox(width: 16),
Expanded(
flex: 2,
child: SizedBox(
height: 300,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
legend(
label: data.totalQueries.label,
color: data.totalQueries.color,
primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numDnsQueries, Platform.localeName),
secondaryValue: "${doubleFormat(serversProvider.serverStatus.data!.stats.avgProcessingTime*1000, Platform.localeName)} ms",
),
const SizedBox(height: 16),
if (data.blockedFilters != null) legend(
label: data.blockedFilters!.label,
color: data.blockedFilters!.color,
primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numBlockedFiltering, Platform.localeName),
secondaryValue: "${serversProvider.serverStatus.data!.stats.numDnsQueries > 0 ? doubleFormat((serversProvider.serverStatus.data!.stats.numBlockedFiltering/serversProvider.serverStatus.data!.stats.numDnsQueries)*100, Platform.localeName) : 0}%",
),
const SizedBox(height: 16),
if (data.replacedSafeBrowsing != null) legend(
label: data.replacedSafeBrowsing!.label,
color: data.replacedSafeBrowsing!.color,
primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numReplacedSafebrowsing, Platform.localeName),
secondaryValue: "${serversProvider.serverStatus.data!.stats.numDnsQueries > 0 ? doubleFormat((serversProvider.serverStatus.data!.stats.numReplacedSafebrowsing/serversProvider.serverStatus.data!.stats.numDnsQueries)*100, Platform.localeName) : 0}%",
),
const SizedBox(height: 16),
if (data.replacedParental != null) legend(
label: data.replacedParental!.label,
color: data.replacedParental!.color,
primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numReplacedParental, Platform.localeName),
secondaryValue: "${serversProvider.serverStatus.data!.stats.numDnsQueries > 0 ? doubleFormat((serversProvider.serverStatus.data!.stats.numReplacedParental/serversProvider.serverStatus.data!.stats.numDnsQueries)*100, Platform.localeName) : 0}%",
),
],
),
)
),
],
)
],
);
}
else {
return Column(
children: [
Column(
children: [
Text(
AppLocalizations.of(context)!.statistics,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface
),
),
const SizedBox(height: 16),
SizedBox(
height: 300,
width: double.maxFinite,
child: CustomCombinedLineChart(
inputData: data,
),
),
const SizedBox(height: 16),
legend(
label: data.totalQueries.label,
color: data.totalQueries.color,
primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numDnsQueries, Platform.localeName),
secondaryValue: "${doubleFormat(serversProvider.serverStatus.data!.stats.avgProcessingTime*1000, Platform.localeName)} ms",
),
const SizedBox(height: 16),
if (data.blockedFilters != null) legend(
label: data.blockedFilters!.label,
color: data.blockedFilters!.color,
primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numBlockedFiltering, Platform.localeName),
secondaryValue: "${serversProvider.serverStatus.data!.stats.numDnsQueries > 0 ? doubleFormat((serversProvider.serverStatus.data!.stats.numBlockedFiltering/serversProvider.serverStatus.data!.stats.numDnsQueries)*100, Platform.localeName) : 0}%",
),
const SizedBox(height: 16),
if (data.replacedSafeBrowsing != null) legend(
label: data.replacedSafeBrowsing!.label,
color: data.replacedSafeBrowsing!.color,
primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numReplacedSafebrowsing, Platform.localeName),
secondaryValue: "${serversProvider.serverStatus.data!.stats.numDnsQueries > 0 ? doubleFormat((serversProvider.serverStatus.data!.stats.numReplacedSafebrowsing/serversProvider.serverStatus.data!.stats.numDnsQueries)*100, Platform.localeName) : 0}%",
),
const SizedBox(height: 16),
if (data.replacedParental != null) legend(
label: data.replacedParental!.label,
color: data.replacedParental!.color,
primaryValue: intFormat(serversProvider.serverStatus.data!.stats.numReplacedParental, Platform.localeName),
secondaryValue: "${serversProvider.serverStatus.data!.stats.numDnsQueries > 0 ? doubleFormat((serversProvider.serverStatus.data!.stats.numReplacedParental/serversProvider.serverStatus.data!.stats.numDnsQueries)*100, Platform.localeName) : 0}%",
),
const SizedBox(height: 16),
],
),
Divider(
thickness: 1,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2),
),
const SizedBox(height: 16)
],
);
}
}
else {
return const SizedBox();
}
}
}

View file

@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/home/server_status.dart';
import 'package:adguard_home_manager/screens/home/combined_chart.dart';
import 'package:adguard_home_manager/screens/home/appbar.dart';
import 'package:adguard_home_manager/screens/home/fab.dart';
import 'package:adguard_home_manager/screens/home/top_items.dart';
@ -110,15 +111,15 @@ class _HomeState extends State<Home> {
return [
ServerStatus(serverStatus: serversProvider.serverStatus.data!),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Divider(
thickness: 1,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2),
),
),
const SizedBox(height: 20),
const SizedBox(height: 16),
Wrap(
if (appConfigProvider.combinedChartHome == false) Wrap(
children: [
FractionallySizedBox(
widthFactor: width > 700 ? 0.5 : 1,
@ -160,10 +161,14 @@ class _HomeState extends State<Home> {
color: Colors.orange,
),
),
],
),
if (appConfigProvider.combinedChartHome == true) const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: CombinedHomeChart(),
),
if (width <= 700) ...[
TopItems(
label: AppLocalizations.of(context)!.topQueriedDomains,
@ -171,14 +176,14 @@ class _HomeState extends State<Home> {
type: 'topQueriedDomains',
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Divider(
thickness: 1,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2),
),
),
const SizedBox(height: 20),
const SizedBox(height: 16),
TopItems(
label: AppLocalizations.of(context)!.topBlockedDomains,
@ -186,13 +191,13 @@ class _HomeState extends State<Home> {
type: 'topBlockedDomains',
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Divider(
thickness: 1,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2),
),
),
const SizedBox(height: 20),
const SizedBox(height: 16),
TopItems(
label: AppLocalizations.of(context)!.topClients,

View file

@ -18,19 +18,24 @@ class ServerStatus extends StatelessWidget {
final width = MediaQuery.of(context).size.width;
return Container(
padding: const EdgeInsets.only(left: 20, right: 20, bottom: 10),
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
child: Column(
children: [
Text(
AppLocalizations.of(context)!.serverStatus,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface
Padding(
padding: width > 700
? const EdgeInsets.all(16)
: const EdgeInsets.all(0),
child: Text(
AppLocalizations.of(context)!.serverStatus,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface
),
),
),
SizedBox(
height: width > 700 ? 90 : 170,
height: width > 700 ? 70 : 170,
child: GridView(
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(

View file

@ -156,6 +156,28 @@ class _GeneralSettingsState extends State<GeneralSettings> {
right: 10
)
),
CustomListTile(
icon: Icons.show_chart_rounded,
title: AppLocalizations.of(context)!.combinedChart,
subtitle: AppLocalizations.of(context)!.combinedChartDescription,
trailing: Switch(
value: appConfigProvider.combinedChartHome,
onChanged: (value) => updateSettings(
newStatus: value,
function: appConfigProvider.setCombinedChartHome
),
),
onTap: () => updateSettings(
newStatus: !appConfigProvider.combinedChartHome,
function: appConfigProvider.setCombinedChartHome
),
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
left: 16,
right: 10
)
),
SectionLabel(label: AppLocalizations.of(context)!.logs),
CustomListTile(
icon: Icons.timer_rounded,

View file

@ -74,6 +74,7 @@ Future<Map<String, dynamic>> loadDb(bool acceptsDynamicTheme) async {
Future upgradeDbToV8(Database db) async {
await db.execute("ALTER TABLE appConfig RENAME COLUMN showNameTimeLogs TO showTimeLogs");
await db.execute("ALTER TABLE appConfig ADD COLUMN showIpLogs NUMERIC");
await db.execute("ALTER TABLE appConfig ADD COLUMN combinedChart NUMERIC");
await db.transaction((txn) async{
await txn.rawQuery(
@ -87,8 +88,8 @@ Future<Map<String, dynamic>> loadDb(bool acceptsDynamicTheme) async {
version: 8,
onCreate: (Database db, int version) async {
await db.execute("CREATE TABLE servers (id TEXT PRIMARY KEY, name TEXT, connectionMethod TEXT, domain TEXT, path TEXT, port INTEGER, user TEXT, password TEXT, defaultServer INTEGER, authToken TEXT, runningOnHa INTEGER)");
await db.execute("CREATE TABLE appConfig (theme NUMERIC, overrideSslCheck NUMERIC, hideZeroValues NUMERIC, useDynamicColor NUMERIC, staticColor NUMERIC, useThemeColorForStatus NUMERIC, showTimeLogs NUMERIC, showIpLogs, doNotRememberVersion TEXT)");
await db.execute("INSERT INTO appConfig (theme, overrideSslCheck, hideZeroValues, useDynamicColor, staticColor, useThemeColorForStatus, showTimeLogs, showIpLogs) VALUES (0, 0, 0, ${acceptsDynamicTheme == true ? 1 : 0}, 0, 0, 0, 0)");
await db.execute("CREATE TABLE appConfig (theme NUMERIC, overrideSslCheck NUMERIC, hideZeroValues NUMERIC, useDynamicColor NUMERIC, staticColor NUMERIC, useThemeColorForStatus NUMERIC, showTimeLogs NUMERIC, showIpLogs NUMERIC, combinedChart NUMERIC, doNotRememberVersion TEXT)");
await db.execute("INSERT INTO appConfig (theme, overrideSslCheck, hideZeroValues, useDynamicColor, staticColor, useThemeColorForStatus, showTimeLogs, showIpLogs, combinedChart) VALUES (0, 0, 0, ${acceptsDynamicTheme == true ? 1 : 0}, 0, 0, 0, 0, 0)");
},
onUpgrade: (Database db, int oldVersion, int newVersion) async {
if (oldVersion == 1) {

View file

@ -0,0 +1,134 @@
import 'package:adguard_home_manager/screens/home/combined_chart.dart';
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:provider/provider.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
class CustomCombinedLineChart extends StatelessWidget {
final CombinedChartData inputData;
const CustomCombinedLineChart({
Key? key,
required this.inputData,
}) : super(key: key);
LineChartData mainData(Map<String, dynamic> data, ThemeMode selectedTheme) {
return LineChartData(
gridData: FlGridData(
show: false,
drawVerticalLine: false,
),
titlesData: FlTitlesData(
show: false,
),
borderData: FlBorderData(
show: false,
),
lineBarsData: List<LineChartBarData>.from(
data["lines"].map((item) => LineChartBarData(
spots: item['data'],
color: item['color'],
isCurved: true,
barWidth: 2,
isStrokeCapRound: true,
preventCurveOverShooting: true,
dotData: FlDotData(
show: false,
),
belowBarData: BarAreaData(
show: true,
color: item['color'].withOpacity(0.2)
),
))
),
lineTouchData: LineTouchData(
enabled: true,
touchTooltipData: LineTouchTooltipData(
tooltipBgColor: selectedTheme == ThemeMode.light
? const Color.fromRGBO(220, 220, 220, 0.9)
: const Color.fromRGBO(35, 35, 35, 0.9),
getTooltipItems: (items) => items.asMap().entries.map((item) => LineTooltipItem(
"${data['lines'][item.key]['label']}: ${item.value.y.toInt().toString()}",
TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: data['lines'][item.key]['color']
)
)).toList()
),
)
);
}
@override
Widget build(BuildContext context) {
final appConfigProvider = Provider.of<AppConfigProvider>(context);
Map<String, dynamic> formatData(CombinedChartData unformattedData) {
int topPoint = 0;
List<FlSpot> dataLine(List<int> data) {
final List<FlSpot> formattedData = [];
int xPosition = 0;
for (int i = 0; i < data.length; i++) {
if (data[i] > topPoint) {
topPoint = data[i];
}
formattedData.add(
FlSpot(
xPosition.toDouble(),
data[i].toDouble()
)
);
xPosition++;
}
return formattedData;
}
List<Map<String, dynamic>> toDraw = [];
toDraw.add({
"data": dataLine(unformattedData.totalQueries.data),
"color": unformattedData.totalQueries.color,
"label": unformattedData.totalQueries.label
});
if (unformattedData.blockedFilters != null) {
toDraw.add({
"data": dataLine(unformattedData.blockedFilters!.data),
"color": unformattedData.blockedFilters!.color,
"label": unformattedData.blockedFilters!.label
});
}
if (unformattedData.replacedSafeBrowsing != null) {
toDraw.add({
"data": dataLine(unformattedData.replacedSafeBrowsing!.data),
"color": unformattedData.replacedSafeBrowsing!.color,
"label": unformattedData.replacedSafeBrowsing!.label
});
}
if (unformattedData.replacedParental != null) {
toDraw.add({
"data": dataLine(unformattedData.replacedParental!.data),
"color": unformattedData.replacedParental!.color,
"label": unformattedData.replacedParental!.label
});
}
return {
'lines': toDraw,
'topPoint': topPoint
};
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 15),
child: LineChart(
mainData(formatData(inputData), appConfigProvider.selectedTheme)
),
);
}
}