diff --git a/lib/config/home_top_items_default_order.dart b/lib/config/home_top_items_default_order.dart index 5220987..a5a23f0 100644 --- a/lib/config/home_top_items_default_order.dart +++ b/lib/config/home_top_items_default_order.dart @@ -5,7 +5,9 @@ import 'package:adguard_home_manager/constants/enums.dart'; final List homeTopItemsDefaultOrder = [ HomeTopItems.queriedDomains, HomeTopItems.blockedDomains, - HomeTopItems.recurrentClients + HomeTopItems.recurrentClients, + HomeTopItems.topUpstreams, + HomeTopItems.avgUpstreamResponseTime ]; final String homeTopItemsDefaultOrderString = jsonEncode( diff --git a/lib/constants/enums.dart b/lib/constants/enums.dart index fed648e..f54e665 100644 --- a/lib/constants/enums.dart +++ b/lib/constants/enums.dart @@ -1,2 +1,2 @@ enum LoadStatus { loading, loaded, error } -enum HomeTopItems { queriedDomains, blockedDomains, recurrentClients } \ No newline at end of file +enum HomeTopItems { queriedDomains, blockedDomains, recurrentClients, topUpstreams, avgUpstreamResponseTime } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7c126ba..b9ba629 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -686,5 +686,7 @@ "unsupportedServerVersion": "Unsupported server version", "unsupportedServerVersionMessage": "Your AdGuard Home server version is too old and is not supported by AdGuard Home Manager. You will need to upgrade your AdGuard Home server to a newer version to use this application.", "yourVersion": "Your version: {version}", - "minimumRequiredVersion": "Minimum required version: {version}" + "minimumRequiredVersion": "Minimum required version: {version}", + "topUpstreams": "Top upstreams", + "averageUpstreamResponseTime": "Average upstream response time" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 9033460..0e1566c 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -686,5 +686,7 @@ "unsupportedServerVersion": "Versión del servidor no soportada", "unsupportedServerVersionMessage": "La versión de tu servidor AdGuard Home es demasiado antigua y no está soportada por AdGuard Home Manager. Necesitarás actualizar tu servidor AdGuard Home a una versión más actual para utilizar esta aplicación.", "yourVersion": "Tu versión: {version}", - "minimumRequiredVersion": "Versión mínima requerida: {version}" + "minimumRequiredVersion": "Versión mínima requerida: {version}", + "topUpstreams": "DNS de subida más frecuentes", + "averageUpstreamResponseTime": "Tiempo promedio de respuesta upstream" } \ No newline at end of file diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 1fdb739..06c3b08 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -176,7 +176,7 @@ "generalSettings": "Genel ayarlar", "generalSettingsDescription": "Çeşitli farklı ayarları yönet", "hideZeroValues": "Sıfır değerlerini gizle", - "hideZeroValuesDescription": "Ana ekranda, değeri sıfır olan blokları gizler", + "hideZeroValuesDescription": "Ana ekranda, değeri sıfır olan blokları gizler.", "webAdminPanel": "Web yönetim paneli", "visitGooglePlay": "Google Play sayfasını ziyaret et", "gitHub": "Kaynak kodlarına GitHub'dan ulaşabilirsiniz", @@ -192,17 +192,17 @@ "enableFiltering": "Filtrelemeyi etkinleştir", "enableSafeBrowsing": "Güvenli gezintiyi etkinleştir", "enableParentalControl": "Ebeveyn kontrolünü etkinleştir", - "enableSafeSearch": "Güvenli aramayı etkinleştir", + "enableSafeSearch": "Güvenli aramayı aktif et", "blockedServices": "Engellenen hizmetler", "selectBlockedServices": "Engellenen hizmetleri seç", "noBlockedServicesSelected": "Engellenen hizmetler seçilmedi", "services": "Hizmetler", "servicesBlocked": "Hizmetler engellendi", "tagsSelected": "Seçilen etiketler", - "upstreamServers": "Üst akış sunucuları", + "upstreamServers": "Üst kaynak sunucuları", "serverAddress": "Sunucu adresi", - "noUpstreamServers": "Üst akış sunucusu yok.", - "willBeUsedGeneralServers": "Genel üst akış sunucuları kullanılacak.", + "noUpstreamServers": "Üst kaynak sunucusu yok.", + "willBeUsedGeneralServers": "Genel üst kaynak sunucuları kullanılacak.", "added": "Eklenenler", "clientUpdatedSuccessfully": "İstemci başarıyla güncellendi", "clientNotUpdated": "İstemci güncellenemedi", @@ -283,7 +283,7 @@ "clientsNotLoaded": "İstemciler yüklenemedi.", "noAllowedClients": "İzin verilmiş istemci yok", "allowedClientsDescription": "Eğer bu liste girdiler içeriyorsa, AdGuard Home yalnızca bu istemcilerden gelen talepleri kabul edecektir.", - "blockedClientsDescription": "Bu liste girdiler içeriyorsa, AdGuard Home bu istemcilerden gelen talepleri reddedecektir. Bu alan, İzin Verilen İstemciler'de girdi varsa görmezden gelinir.", + "blockedClientsDescription": "Bu liste girdileri içeriyorsa, AdGuard Home bu istemcilerden gelen talepleri reddedecektir. Bu alan adı, İzin Verilen İstemciler'de girdi varsa görmezden gelinir.", "disallowedDomainsDescription": "AdGuard Home, bu alan adlarına uyan DNS sorgularını reddeder ve bu sorgular sorgu günlüğünde bile görünmez.", "addClientFieldDescription": "CIDR'ler, IP adresi veya ClientID", "clientIdentifier": "İstemci tanımlayıcısı", @@ -293,15 +293,15 @@ "domainNotAdded": "Alan adı eklenemedi", "statusSelected": "Durum seçildi.", "updateLists": "Listeleri güncelle", - "checkHostFiltered": "Ana bilgisayarı kontrol et", + "checkHostFiltered": "Filtrelemeyi kontrol et", "updatingLists": "Listeler güncelleniyor...", "listsUpdated": "Listeler güncellendi", "listsNotUpdated": "Listeler güncellenemedi", "listsNotLoaded": "Listeler yüklenemedi", "domainNotValid": "Alan adı geçersiz", "check": "Kontrol et", - "checkingHost": "Ana bilgisayar kontrol ediliyor", - "errorCheckingHost": "Ana bilgisayar kontrol edilemedi", + "checkingHost": "Kontrol ediliyor", + "errorCheckingHost": "Kontrol etme başarısız", "block": "Engelle", "unblock": "Engeli kaldır", "custom": "Özel", @@ -336,7 +336,7 @@ "updating": "Değerler güncelleniyor...", "blockedServicesUpdated": "Engellenen hizmetler başarıyla güncellendi", "blockedServicesNotUpdated": "Engellenen hizmetler güncellenemedi", - "insertDomain": "Durumu kontrol etmek için bir alan adı ekleyin.", + "insertDomain": "Filtreleme durumunu kontrol etmek için bir alan adı ekleyin.", "dhcpSettings": "DHCP ayarları", "dhcpSettingsDescription": "DHCP sunucusunu yapılandır", "dhcpSettingsNotLoaded": "DHCP ayarları yüklenemedi", @@ -394,18 +394,18 @@ "noLeases": "Kullanılabilir DHCP kiralaması yok", "dnsRewrites": "DNS yeniden yazımları", "dnsRewritesDescription": "Özel DNS kurallarını yapılandır", - "loadingRewriteRules": "Yeniden yazma kuralları yükleniyor...", - "rewriteRulesNotLoaded": "DNS yeniden yazma kuralları yüklenemedi.", + "loadingRewriteRules": "Yeniden yazım kuralları yükleniyor...", + "rewriteRulesNotLoaded": "DNS yeniden yazım kuralları yüklenemedi.", "noRewriteRules": "DNS yeniden yazım kuralları yok", "answer": "Yanıt", "deleteDnsRewrite": "DNS yeniden yazımı sil", - "deleteDnsRewriteMessage": "Bu DNS yeniden yazmasını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", - "dnsRewriteRuleDeleted": "DNS yeniden yazma kuralı başarıyla silindi", - "dnsRewriteRuleNotDeleted": "DNS yeniden yazma kuralı silinemedi", + "deleteDnsRewriteMessage": "Bu DNS yeniden yazımını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", + "dnsRewriteRuleDeleted": "DNS yeniden yazım kuralı başarıyla silindi", + "dnsRewriteRuleNotDeleted": "DNS yeniden yazım kuralı silinemedi", "addDnsRewrite": "DNS yeniden yazımı ekle", - "addingRewrite": "Yeniden yazma ekleniyor...", - "dnsRewriteRuleAdded": "DNS yeniden yazma kuralı başarıyla eklendi", - "dnsRewriteRuleNotAdded": "DNS yeniden yazma kuralı eklenemedi", + "addingRewrite": "Yeniden yazım ekleniyor...", + "dnsRewriteRuleAdded": "DNS yeniden yazım kuralı başarıyla eklendi", + "dnsRewriteRuleNotAdded": "DNS yeniden yazım kuralı eklenemedi", "logsSettings": "Günlük ayarları", "enableLog": "Günlüğü etkinleştir", "clearLogs": "Günlükleri temizle", @@ -422,33 +422,33 @@ "deletingLogs": "Günlükler temizleniyor...", "logsCleared": "Günlükler başarıyla temizlendi", "logsNotCleared": "Günlükler temizlenemedi", - "runningHomeAssistant": "Ev Asistanı üzerinde çalıştır", + "runningHomeAssistant": "Ev asistanı üzerinde çalıştır", "serverError": "Sunucu hatası", "noItems": "Burada gösterilecek öğe yok", "dnsSettings": "DNS ayarları", "dnsSettingsDescription": "DNS sunucuları ile bağlantıyı yapılandır", - "upstreamDns": "Üst akış DNS sunucuları", + "upstreamDns": "Üst kaynak DNS sunucuları", "bootstrapDns": "Önyükleme DNS sunucuları", - "noUpstreamDns": "Üst akış DNS sunucuları eklenmedi.", + "noUpstreamDns": "Üst kaynak DNS sunucuları eklenmedi.", "dnsMode": "DNS modu", "noDnsMode": "DNS modu seçili değil", "loadBalancing": "Yük dengeleme", "parallelRequests": "Paralel istekler", "fastestIpAddress": "En hızlı IP adresi", - "loadBalancingDescription": "Her seferinde bir üst akış sunucusuna sorgu yap. AdGuard Home, sunucuyu seçmek için ağırlıklı rastgele algoritmasını kullanır, böylece en hızlı sunucu daha sık kullanılır.", - "parallelRequestsDescription": "Tüm üst akış sunucularını aynı anda sorgulayarak çözümlemeyi hızlandırmak için paralel sorgular kullanın.", + "loadBalancingDescription": "Her seferinde bir üst kaynak sunucusuna sorgu yap. AdGuard Home, sunucuyu seçmek için ağırlıklı rastgele algoritmasını kullanır, böylece en hızlı sunucu daha sık kullanılır.", + "parallelRequestsDescription": "Tüm üst kaynak sunucularını aynı anda sorgulayarak çözümlemeyi hızlandırmak için paralel sorgular kullanın.", "fastestIpAddressDescription": "Tüm DNS sunucularına sorgu yapın ve tüm yanıtlar arasında en hızlı IP adresini döndürün. Bu, AdGuard Home'un tüm DNS sunucularından yanıtları beklemesi gerektiği için DNS sorgularını yavaşlatır, ancak genel bağlantıyı iyileştirir.", "noBootstrapDns": "Önyükleme DNS sunucuları eklenmedi.", - "bootstrapDnsServersInfo": "Önyükleme ​​DNS sunucuları, üst akışlarda belirttiğiniz DoH/DoT çözümleyicilerinin IP adreslerini çözmek için kullanılır.", + "bootstrapDnsServersInfo": "Önyükleme ​​DNS sunucuları, üst kaynaklarda belirttiğiniz DoH/DoT çözümleyicilerinin IP adreslerini çözmek için kullanılır.", "privateReverseDnsServers": "Özel ters DNS sunucuları", "privateReverseDnsServersDescription": "AdGuard Home'un yerel PTR sorguları için kullandığı DNS sunucuları. Bu sunucular, özel IP aralıklarındaki adresler için ters DNS kullanarak PTR isteklerini çözmek için kullanılır, örneğin '192.168.12.34' olarak ayarlanmamışsa AdGuard Home, AdGuard Home'un kendi adresleri dışında, işletim sisteminizin varsayılan DNS çözümleyicilerinin adreslerini kullanır.", "reverseDnsDefault": "Varsayılan olarak, AdGuard Home aşağıdaki ters DNS çözümleyicilerini kullanır", "addItem": "Öğe ekle", "noServerAddressesAdded": "Sunucu adresleri eklenmedi.", "usePrivateReverseDnsResolvers": "Özel ters DNS çözümleyicilerini kullan", - "usePrivateReverseDnsResolversDescription": "Bu üst akış sunucularını kullanarak yerel olarak sunulan adresler için ters DNS sorguları gerçekleştirin. Devre dışı bırakılırsa, AdGuard Home, DHCP, /etc/hosts vb. kaynaklardan bilinen istemciler dışında tüm PTR isteklerine NXDOMAIN yanıtı verir.", + "usePrivateReverseDnsResolversDescription": "Bu üst kaynak sunucularını kullanarak yerel olarak sunulan adresler için ters DNS sorguları gerçekleştirin. Devre dışı bırakılırsa, AdGuard Home, DHCP, /etc/hosts vb. kaynaklardan bilinen istemciler dışında tüm PTR isteklerine NXDOMAIN yanıtı verir.", "enableReverseResolving": "İstemcilerin IP adreslerinin ters çözümlemesini etkinleştir", - "enableReverseResolvingDescription": "İstemcilerin IP adreslerini karşılık gelen çözücülere PTR sorguları göndererek IP adreslerini tersine çözümleyerek (yerel istemciler için özel DNS sunucuları, genel IP adresine sahip istemciler için yukarı akış sunucuları) istemcilerin ana bilgisayar adlarını tersine çöz.", + "enableReverseResolvingDescription": "İstemcilerin IP adreslerini karşılık gelen çözücülere PTR sorguları göndererek IP adreslerini tersine çözümleyerek (yerel istemciler için özel DNS sunucuları, genel IP adresine sahip istemciler için üst kaynak sunucuları) istemcilerin ana bilgisayar adlarını tersine çöz.", "dnsServerSettings": "AdGuard Home DNS sunucusu ayarları", "limitRequestsSecond": "Saniye başına sınırlama isteği", "valueNotNumber": "Değer bir sayı değil", @@ -460,13 +460,13 @@ "disableResolvingIpv6Description": "IPv6 adresleri için tüm DNS sorgularını bırakın (AAAA yazın) ve HTTPS yanıtlarından IPv6 ipuçlarını kaldırın.", "blockingMode": "Engelleme modu", "defaultMode": "Varsayılan", - "defaultDescription": "Reklam engelleme tarzı bir kural tarafından engellendiğinde sıfır IP adresi ile yanıt verin (A için 0.0.0.0; :: AAAA için) /etc/hosts tarzı bir kural tarafından engellendiğinde kuralda belirtilen IP adresi ile yanıt verin", - "refusedDescription": "REFUSED kodu ile yanıt verin", - "nxdomainDescription": "NXDOMAIN kodu ile yanıt verin", + "defaultDescription": "Reklam engelleme tarzı bir kural tarafından engellendiğinde sıfır IP adresi ile yanıt verin. (A için 0.0.0.0; :: AAAA için) /etc/hosts tarzı bir kural tarafından engellendiğinde kuralda belirtilen IP adresi ile yanıt verin.", + "refusedDescription": "REFUSED kodu ile yanıt verin.", + "nxdomainDescription": "NXDOMAIN kodu ile yanıt verin.", "nullIp": "Boş IP", - "nullIpDescription": "Sıfır IP adresi ile yanıt verin (A için 0.0.0.0; :: AAAA için)", + "nullIpDescription": "Sıfır IP adresi ile yanıt verin. (A için 0.0.0.0; :: AAAA için)", "customIp": "Özel IP", - "customIpDescription": "Manuel olarak ayarlanmış bir IP adresi ile yanıt verin", + "customIpDescription": "Manuel olarak ayarlanmış bir IP adresi ile yanıt verin.", "dnsCacheConfig": "DNS önbellek yapılandırması", "cacheSize": "Önbellek boyutu", "inBytes": "Bayt olarak", @@ -487,7 +487,7 @@ "dnsConfigNotSaved": "DNS sunucusu yapılandırması kaydedilemedi", "savingConfig": "Yapılandırma kaydediliyor...", "someValueNotValid": "Bazı değerler geçerli değil", - "upstreamDnsDescription": "Üst akış sunucularını ve DNS modunu yapılandır", + "upstreamDnsDescription": "Üst kaynak sunucularını ve DNS modunu yapılandır", "bootstrapDnsDescription": "Önyükleme DNS sunucularını yapılandır", "privateReverseDnsDescription": "Özel DNS çözümleyicileri yapılandır ve özel ters DNS çözümlemeyi etkinleştir", "dnsServerSettingsDescription": "Hız limiti, engelleme modu ve daha fazlasını yapılandır", @@ -551,7 +551,7 @@ "deepOrange": "Koyu turuncu", "indigo": "Çivit mavisi", "useThemeColorStatus": "Durum için tema rengini kullan", - "useThemeColorStatusDescription": "Yeşil ve kırmızı durum renklerini tema rengi ve gri ile değiştirir", + "useThemeColorStatusDescription": "Yeşil ve kırmızı durum renklerini tema rengi ve gri ile değiştirir.", "invalidCertificateChain": "Geçersiz sertifika zinciri", "validCertificateChain": "Geçerli sertifika zinciri", "subject": "Konu", @@ -624,18 +624,18 @@ "unsupprtedVersionMessage": "Sunucu sürümünüz {version} için destek garantisi verilmiyor. Bu uygulamanın bu sunucu sürümüyle çalışmasında bazı sorunlar olabilir. AdGuard Home Yöneticisi, AdGuard Home sunucunun kararlı sürümleriyle çalışacak şekilde tasarlanmıştır. Alfa ve beta sürümleriyle çalışabilir, ancak uyumluluk garanti edilmez ve uygulama bu sürümlerle çalışırken bazı sorunlar yaşayabilir.", "iUnderstand": "Anladım", "appUpdates": "Uygulama güncellemeleri", - "usingLatestVersion": "En son sürümü kullanıyorsunuz", + "usingLatestVersion": "En son sürümü kullanıyorsunuz :)", "ipLogs": "Günlüklerdeki IP", "ipLogsDescription": "Günlükler listesinde istemci adı yerine IP adresini göster", "application": "Uygulama", "combinedChart": "Birleştirilmiş grafik", - "combinedChartDescription": "Tüm grafikleri bir araya getirir", + "combinedChartDescription": "Tüm grafikleri bir araya getirir.", "statistics": "İstatistikler", "errorLoadFilters": "Filtreler yüklenirken hata oluştu.", "clientRemovedSuccessfully": "İstemci başarıyla kaldırıldı.", - "editRewriteRule": "Yeniden yazma kuralını düzenle", - "dnsRewriteRuleUpdated": "DNS yeniden yazma kuralı başarıyla güncellendi", - "dnsRewriteRuleNotUpdated": "DNS yeniden yazma kuralı güncellenemedi", + "editRewriteRule": "Yeniden yazım kuralını düzenle", + "dnsRewriteRuleUpdated": "DNS yeniden yazım kuralı başarıyla güncellendi", + "dnsRewriteRuleNotUpdated": "DNS yeniden yazım kuralı güncellenemedi", "updatingRule": "Kural güncelleniyor...", "serverUpdateNeeded": "Sunucu güncellemesi gerekli", "updateYourServer": "Bu özelliği kullanmak için AdGuard Home sunucunuzu {version} veya üzeri bir sürüme güncelleyin.", @@ -657,7 +657,7 @@ "quickFilters": "Hızlı filtreler", "searchDomainInternet": "İnternette alan adı ara", "hideServerAddress": "Sunucu adresini gizle", - "hideServerAddressDescription": "Ana ekranda sunucu adresini gizler", + "hideServerAddressDescription": "Ana ekranda sunucu adresini gizler.", "topItemsOrder": "Öne çıkan öğeler sıralaması", "topItemsOrderDescription": "Ana ekrandaki öne çıkan öğe listelerini sırala", "topItemsReorderInfo": "Yeniden sıralamak için bir öğeyi basılı tutun ve kaydırın.", @@ -682,9 +682,11 @@ "processingLists": "Listeler işleniyor...", "enableDisableResult": "Sonucu etkinleştir veya devre dışı bırak", "selectedListsEnabledDisabledSuccessfully": "Seçilen tüm listeler başarıyla etkinleştirildi veya devre dışı bırakıldı", - "sslWarning": "Kendinden imzalı bir sertifika ile HTTPS bağlantısı kullanıyorsanız, Ayarlar > Gelişmiş ayarlar bölümünde \"SSL sertifikasını kontrol etme\" seçeneğini etkinleştirdiğinizden emin olun.", + "sslWarning": "Kendinden imzalı bir sertifika ile HTTPS bağlantısı kullanıyorsanız, Ayarlar > Gelişmiş ayarlar bölümünde \"SSL sertifikasını asla kontrol etme\" seçeneğini etkinleştirdiğinizden emin olun.", "unsupportedServerVersion": "Desteklenmeyen sunucu sürümü", "unsupportedServerVersionMessage": "AdGuard Home sunucu sürümünüz çok eski ve AdGuard Home Manager tarafından desteklenmiyor. Bu uygulamayı kullanmak için AdGuard Home sunucunuzu daha yeni bir sürüme yükseltmeniz gerekecektir.", "yourVersion": "Yüklü sürüm: {version}", - "minimumRequiredVersion": "Gerekli minimum sürüm: {version}" + "minimumRequiredVersion": "Gerekli minimum sürüm: {version}", + "topUpstreams": "Öne çıkan üst kaynaklar", + "averageUpstreamResponseTime": "Üst kaynak ortalama yanıt süresi" } diff --git a/lib/models/dns_statistics.dart b/lib/models/dns_statistics.dart index 45993bd..f279886 100644 --- a/lib/models/dns_statistics.dart +++ b/lib/models/dns_statistics.dart @@ -9,6 +9,8 @@ class DnsStatistics { final List> topQueriedDomains; final List> topClients; final List> topBlockedDomains; + final List>? topUpstreamResponses; + final List>? topUpstreamsAvgTime; final List dnsQueries; final List blockedFiltering; final List replacedSafebrowsing; @@ -25,6 +27,8 @@ class DnsStatistics { required this.topQueriedDomains, required this.topClients, required this.topBlockedDomains, + required this.topUpstreamResponses, + required this.topUpstreamsAvgTime, required this.dnsQueries, required this.blockedFiltering, required this.replacedSafebrowsing, @@ -42,6 +46,8 @@ class DnsStatistics { topQueriedDomains: List>.from(json["top_queried_domains"].map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))), topClients: List>.from(json["top_clients"].map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))), topBlockedDomains: List>.from(json["top_blocked_domains"].map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))), + topUpstreamResponses: json["top_upstreams_responses"] != null ? List>.from(json["top_upstreams_responses"].map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))) : null, + topUpstreamsAvgTime: json["top_upstreams_avg_time"] != null ? List>.from(json["top_upstreams_avg_time"].map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))) : null, dnsQueries: List.from(json["dns_queries"].map((x) => x)), blockedFiltering: List.from(json["blocked_filtering"].map((x) => x)), replacedSafebrowsing: List.from(json["replaced_safebrowsing"].map((x) => x)), @@ -59,6 +65,8 @@ class DnsStatistics { "top_queried_domains": List.from(topQueriedDomains.map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))), "top_clients": List.from(topClients.map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))), "top_blocked_domains": List.from(topBlockedDomains.map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))), + "top_upstreams_responses": topUpstreamResponses != null ? List.from(topUpstreamResponses!.map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))) : null, + "top_upstreams_avg_time": topUpstreamsAvgTime != null ? List.from(topUpstreamsAvgTime!.map((x) => Map.from(x).map((k, v) => MapEntry(k, v)))) : null, "dns_queries": List.from(dnsQueries.map((x) => x)), "blocked_filtering": List.from(blockedFiltering.map((x) => x)), "replaced_safebrowsing": List.from(replacedSafebrowsing.map((x) => x)), diff --git a/lib/models/encryption.dart b/lib/models/encryption.dart index 8a384c8..1315868 100644 --- a/lib/models/encryption.dart +++ b/lib/models/encryption.dart @@ -124,12 +124,19 @@ class EncryptionData { } +class EncryptionValidationResult { + final bool isObject; + final EncryptionValidation? encryptionValidation; + final String? message; -EncyptionValidation encyptionValidationFromJson(String str) => EncyptionValidation.fromJson(json.decode(str)); + const EncryptionValidationResult({ + required this.isObject, + this.encryptionValidation, + this.message + }); +} -String encyptionValidationToJson(EncyptionValidation data) => json.encode(data.toJson()); - -class EncyptionValidation { +class EncryptionValidation { final String? subject; final String? issuer; final String? keyType; @@ -156,7 +163,7 @@ class EncyptionValidation { final String? privateKeyPath; final bool? privateKeySaved; - EncyptionValidation({ + EncryptionValidation({ this.subject, this.issuer, this.keyType, @@ -184,7 +191,7 @@ class EncyptionValidation { this.privateKeySaved, }); - factory EncyptionValidation.fromJson(Map json) => EncyptionValidation( + factory EncryptionValidation.fromJson(Map json) => EncryptionValidation( subject: json["subject"], issuer: json["issuer"], keyType: json["key_type"], diff --git a/lib/models/menu_option.dart b/lib/models/menu_option.dart index e898514..b89f8d9 100644 --- a/lib/models/menu_option.dart +++ b/lib/models/menu_option.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; class MenuOption { final IconData? icon; final String title; - final void Function() action; + final void Function(dynamic) action; final bool? disabled; const MenuOption({ diff --git a/lib/providers/app_config_provider.dart b/lib/providers/app_config_provider.dart index 16e855f..d44b5af 100644 --- a/lib/providers/app_config_provider.dart +++ b/lib/providers/app_config_provider.dart @@ -449,7 +449,7 @@ class AppConfigProvider with ChangeNotifier { _showTopItemsChart = dbData['showTopItemsChart']; if (dbData['homeTopItemsOrder'] != null) { try { - _homeTopItemsOrder = List.from( + final itemsOrder = List.from( List.from(jsonDecode(dbData['homeTopItemsOrder'])).map((e) { switch (e) { case 'queriedDomains': @@ -461,11 +461,22 @@ class AppConfigProvider with ChangeNotifier { case 'recurrentClients': return HomeTopItems.recurrentClients; + case 'topUpstreams': + return HomeTopItems.topUpstreams; + + case 'avgUpstreamResponseTime': + return HomeTopItems.avgUpstreamResponseTime; + default: return null; } }).where((e) => e != null).toList() ); + final missingItems = homeTopItemsDefaultOrder.where((e) => !itemsOrder.contains(e)); + _homeTopItemsOrder = [ + ...itemsOrder, + ...missingItems + ]; } catch (e) { Sentry.captureException(e); _homeTopItemsOrder = homeTopItemsDefaultOrder; diff --git a/lib/providers/status_provider.dart b/lib/providers/status_provider.dart index 0c519f8..527749f 100644 --- a/lib/providers/status_provider.dart +++ b/lib/providers/status_provider.dart @@ -237,7 +237,7 @@ class StatusProvider with ChangeNotifier { } Future getServerStatus({ - bool? withLoadingIndicator, + bool? withLoadingIndicator = true, bool? overrideCheckServerVersion }) async { if (withLoadingIndicator == true) { diff --git a/lib/screens/clients/added_list.dart b/lib/screens/clients/added_list.dart index 124f32c..0a0bc08 100644 --- a/lib/screens/clients/added_list.dart +++ b/lib/screens/clients/added_list.dart @@ -11,7 +11,6 @@ import 'package:adguard_home_manager/screens/clients/client/client_screen_functi import 'package:adguard_home_manager/screens/clients/client/added_client_tile.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/options_modal.dart'; import 'package:adguard_home_manager/widgets/tab_content_list.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; @@ -143,16 +142,11 @@ class _AddedListState extends State { ) ); } - - void openOptionsModal(Client client) { - showModal( - context: context, - builder: (ctx) => OptionsModal( - onDelete: () => openDeleteModal(client), - onEdit: () => openClientModal(client), - ) - ); - } + final clientsDisplay = clientsProvider.searchTermClients != null && clientsProvider.searchTermClients != "" + ? widget.data.where( + (c) => c.name.toLowerCase().contains(clientsProvider.searchTermClients.toString()) || c.ids.where((id) => id.contains(clientsProvider.searchTermClients.toString())).isNotEmpty + ).toList() + : widget.data; return CustomTabContentList( listPadding: widget.splitView == true @@ -177,12 +171,11 @@ class _AddedListState extends State { ], ), ), - itemsCount: widget.data.length, + itemsCount: clientsDisplay.length, contentWidget: (index) => AddedClientTile( selectedClient: widget.selectedClient, - client: widget.data[index], + client: clientsDisplay[index], onTap: widget.onClientSelected, - onLongPress: openOptionsModal, onEdit: statusProvider.serverStatus != null ? (c) => openClientModal(c) : null, diff --git a/lib/screens/clients/client/active_client_tile.dart b/lib/screens/clients/client/active_client_tile.dart index 26cb30f..76c928c 100644 --- a/lib/screens/clients/client/active_client_tile.dart +++ b/lib/screens/clients/client/active_client_tile.dart @@ -1,9 +1,8 @@ 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/options_menu.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'; @@ -16,54 +15,29 @@ class ActiveClientTile extends StatelessWidget { final AutoClient? selectedClient; const ActiveClientTile({ - Key? key, + super.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, + child: OptionsMenu( + options: [ + MenuOption( icon: Icons.copy_rounded, - onTap: () { - copyToClipboard( - value: client.name != '' - ? client.name! - : client.ip, - successMessage: AppLocalizations.of(context)!.copiedClipboard, - ); - Navigator.pop(context); - }, + title: AppLocalizations.of(context)!.copyClipboard, + action: (_) => copyToClipboard( + value: client.name != '' + ? client.name! + : client.ip, + successMessage: AppLocalizations.of(context)!.copiedClipboard, + ) ) ], child: Material( @@ -72,10 +46,6 @@ class ActiveClientTile extends StatelessWidget { 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), @@ -128,20 +98,17 @@ class ActiveClientTile extends StatelessWidget { ); } else { - return ContextMenuArea( - builder: (context) => [ - CustomListTile( - title: AppLocalizations.of(context)!.copyClipboard, + return OptionsMenu( + options: [ + MenuOption( icon: Icons.copy_rounded, - onTap: () { - copyToClipboard( - value: client.name != '' - ? client.name! - : client.ip, - successMessage: AppLocalizations.of(context)!.copiedClipboard, - ); - Navigator.pop(context); - }, + title: AppLocalizations.of(context)!.copyClipboard, + action: (_) => copyToClipboard( + value: client.name != '' + ? client.name! + : client.ip, + successMessage: AppLocalizations.of(context)!.copiedClipboard, + ) ) ], child: CustomListTile( @@ -158,7 +125,6 @@ class ActiveClientTile extends StatelessWidget { ), ), onTap: () => onTap(client), - onLongPress: openOptionsModal, ), ); } diff --git a/lib/screens/clients/client/added_client_tile.dart b/lib/screens/clients/client/added_client_tile.dart index 53a481a..4f4037e 100644 --- a/lib/screens/clients/client/added_client_tile.dart +++ b/lib/screens/clients/client/added_client_tile.dart @@ -1,18 +1,18 @@ 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/options_menu.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.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'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; -class AddedClientTile extends StatelessWidget { +class AddedClientTile extends StatefulWidget { 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; @@ -22,67 +22,54 @@ class AddedClientTile extends StatelessWidget { super.key, required this.client, required this.onTap, - required this.onLongPress, this.onEdit, required this.onDelete, this.selectedClient, required this.splitView, }); + @override + State createState() => _AddedClientTileState(); +} + +class _AddedClientTileState extends State { + bool _isHover = false; + @override Widget build(BuildContext context) { final appConfigProvider = Provider.of(context); - if (splitView == true) { + if (widget.splitView == true) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Material( color: Colors.transparent, borderRadius: BorderRadius.circular(28), - child: ContextMenuArea( - builder: (context) => [ - if (onEdit != null) 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, + child: OptionsMenu( + options: [ + MenuOption( icon: Icons.copy_rounded, - onTap: () { - copyToClipboard( - value: client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), - successMessage: AppLocalizations.of(context)!.copiedClipboard, - ); - Navigator.pop(context); - } + title: AppLocalizations.of(context)!.copyClipboard, + action: (_) => copyToClipboard( + value: widget.client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), + successMessage: AppLocalizations.of(context)!.copiedClipboard, + ) ), ], child: InkWell( borderRadius: BorderRadius.circular(28), - onTap: () => onTap(client), - onLongPress: () => onLongPress(client), + onTap: () => widget.onTap(widget.client), + onHover: (v) => setState(() => _isHover = v), child: Container( width: double.maxFinite, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( borderRadius: BorderRadius.circular(28), - color: client == selectedClient + color: widget.client == widget.selectedClient ? Theme.of(context).colorScheme.primaryContainer : null ), - child: Row( + child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( @@ -94,7 +81,7 @@ class AddedClientTile extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), + widget.client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w400, @@ -107,7 +94,7 @@ class AddedClientTile extends StatelessWidget { Icon( Icons.filter_list_rounded, size: 19, - color: client.filteringEnabled == true + color: widget.client.filteringEnabled == true ? appConfigProvider.useThemeColorForStatus == true ? Theme.of(context).colorScheme.primary : Colors.green @@ -119,7 +106,7 @@ class AddedClientTile extends StatelessWidget { Icon( Icons.vpn_lock_rounded, size: 18, - color: client.safebrowsingEnabled == true + color: widget.client.safebrowsingEnabled == true ? appConfigProvider.useThemeColorForStatus == true ? Theme.of(context).colorScheme.primary : Colors.green @@ -131,7 +118,7 @@ class AddedClientTile extends StatelessWidget { Icon( Icons.block, size: 18, - color: client.parentalEnabled == true + color: widget.client.parentalEnabled == true ? appConfigProvider.useThemeColorForStatus == true ? Theme.of(context).colorScheme.primary : Colors.green @@ -143,7 +130,7 @@ class AddedClientTile extends StatelessWidget { Icon( Icons.search_rounded, size: 19, - color: client.safeSearch != null && client.safeSearch!.enabled == true + color: widget.client.safeSearch != null && widget.client.safeSearch!.enabled == true ? appConfigProvider.useThemeColorForStatus == true ? Theme.of(context).colorScheme.primary : Colors.green @@ -159,6 +146,14 @@ class AddedClientTile extends StatelessWidget { ], ), ), + if (widget.onEdit != null && _isHover == true) ...[ + const SizedBox(width: 8), + IconButton( + onPressed: () => widget.onEdit!(widget.client), + icon: const Icon(Icons.file_open_rounded), + tooltip: AppLocalizations.of(context)!.seeDetails, + ) + ] ], ) ), @@ -168,37 +163,30 @@ class AddedClientTile extends StatelessWidget { ); } else { - return ContextMenuArea( - builder: (context) => [ - if (onEdit != null) CustomListTile( + return OptionsMenu( + options: [ + if (widget.onEdit != null) MenuOption( title: AppLocalizations.of(context)!.seeDetails, icon: Icons.file_open_rounded, - onTap: () { - Navigator.pop(context); - onEdit!(client); - } + action: (_) => widget.onEdit!(widget.client) ), - CustomListTile( - title: AppLocalizations.of(context)!.copyClipboard, + MenuOption( icon: Icons.copy_rounded, - onTap: () { - copyToClipboard( - value: client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), - successMessage: AppLocalizations.of(context)!.copiedClipboard, - ); - Navigator.pop(context); - } + title: AppLocalizations.of(context)!.copyClipboard, + action: (_) => copyToClipboard( + value: widget.client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), + successMessage: AppLocalizations.of(context)!.copiedClipboard, + ) ), ], child: CustomListTile( - onLongPress: () => onLongPress(client), - onTap: () => onTap(client), - title: client.name, + onTap: () => widget.onTap(widget.client), + title: widget.client.name, subtitleWidget: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), + widget.client.ids.toString().replaceAll(RegExp(r'^\[|\]$'), ''), style: TextStyle( color: Theme.of(context).listTileTheme.textColor ), @@ -209,7 +197,7 @@ class AddedClientTile extends StatelessWidget { Icon( Icons.filter_list_rounded, size: 19, - color: client.filteringEnabled == true + color: widget.client.filteringEnabled == true ? appConfigProvider.useThemeColorForStatus == true ? Theme.of(context).colorScheme.primary : Colors.green @@ -221,7 +209,7 @@ class AddedClientTile extends StatelessWidget { Icon( Icons.vpn_lock_rounded, size: 18, - color: client.safebrowsingEnabled == true + color: widget.client.safebrowsingEnabled == true ? appConfigProvider.useThemeColorForStatus == true ? Theme.of(context).colorScheme.primary : Colors.green @@ -233,7 +221,7 @@ class AddedClientTile extends StatelessWidget { Icon( Icons.block, size: 18, - color: client.parentalEnabled == true + color: widget.client.parentalEnabled == true ? appConfigProvider.useThemeColorForStatus == true ? Theme.of(context).colorScheme.primary : Colors.green @@ -245,7 +233,7 @@ class AddedClientTile extends StatelessWidget { Icon( Icons.search_rounded, size: 19, - color: client.safeSearch != null && client.safeSearch!.enabled == true + color: widget.client.safeSearch != null && widget.client.safeSearch!.enabled == true ? appConfigProvider.useThemeColorForStatus == true ? Theme.of(context).colorScheme.primary : Colors.green diff --git a/lib/screens/clients/clients.dart b/lib/screens/clients/clients.dart index 80393a8..3d494ae 100644 --- a/lib/screens/clients/clients.dart +++ b/lib/screens/clients/clients.dart @@ -7,7 +7,7 @@ import 'package:adguard_home_manager/screens/clients/clients_lists.dart'; import 'package:adguard_home_manager/models/clients.dart'; class Clients extends StatefulWidget { - const Clients({Key? key}) : super(key: key); + const Clients({super.key}); @override State createState() => _ClientsState(); @@ -20,36 +20,38 @@ class _ClientsState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - if (constraints.maxWidth > 1000) { - return SplitView.material( - hideDivider: true, - flexWidth: const FlexWidth(mainViewFlexWidth: 1, secondaryViewFlexWidth: 2), - placeholder: Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Text( - AppLocalizations.of(context)!.selectClientLeftColumn, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSurfaceVariant + return Scaffold( + body: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > 1000) { + return SplitView.material( + hideDivider: true, + flexWidth: const FlexWidth(mainViewFlexWidth: 1, secondaryViewFlexWidth: 2), + placeholder: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + AppLocalizations.of(context)!.selectClientLeftColumn, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant + ), ), ), ), - ), - child: const ClientsLists( - splitView: true, - ) - ); - } - else { - return const ClientsLists( - splitView: false, - ); - } - }, + child: const ClientsLists( + splitView: true, + ) + ); + } + else { + return const ClientsLists( + splitView: false, + ); + } + }, + ), ); } } \ No newline at end of file diff --git a/lib/screens/clients/clients_list.dart b/lib/screens/clients/clients_list.dart index 2e52738..bdff4ee 100644 --- a/lib/screens/clients/clients_list.dart +++ b/lib/screens/clients/clients_list.dart @@ -16,12 +16,12 @@ class ClientsList extends StatelessWidget { final bool splitView; const ClientsList({ - Key? key, + super.key, required this.data, required this.onClientSelected, this.selectedClient, required this.splitView, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/screens/clients/clients_lists.dart b/lib/screens/clients/clients_lists.dart index 0e518ce..bdc56b8 100644 --- a/lib/screens/clients/clients_lists.dart +++ b/lib/screens/clients/clients_lists.dart @@ -18,9 +18,9 @@ class ClientsLists extends StatefulWidget { final bool splitView; const ClientsLists({ - Key? key, + super.key, required this.splitView, - }) : super(key: key); + }); @override State createState() => _ClientsListsState(); diff --git a/lib/screens/clients/options_modal.dart b/lib/screens/clients/options_modal.dart deleted file mode 100644 index d779aae..0000000 --- a/lib/screens/clients/options_modal.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.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'; - -class OptionsModal extends StatelessWidget { - final void Function() onEdit; - final void Function() onDelete; - - const OptionsModal({ - Key? key, - required this.onDelete, - required this.onEdit, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final statusProvider = Provider.of(context); - - return AlertDialog( - contentPadding: const EdgeInsets.symmetric( - horizontal: 0, - vertical: 16 - ), - title: Column( - children: [ - Icon( - Icons.more_horiz, - color: Theme.of(context).listTileTheme.iconColor - ), - const SizedBox(height: 16), - Text( - AppLocalizations.of(context)!.options, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface - ), - ) - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 24), - if (statusProvider.serverStatus != null) CustomListTileDialog( - onTap: () { - Navigator.pop(context); - onEdit(); - }, - title: AppLocalizations.of(context)!.edit, - icon: Icons.edit, - ), - CustomListTileDialog( - onTap: () { - Navigator.pop(context); - onDelete(); - }, - title: AppLocalizations.of(context)!.delete, - icon: Icons.delete, - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(AppLocalizations.of(context)!.close) - ) - ], - ); - } -} \ No newline at end of file diff --git a/lib/screens/clients/search_clients.dart b/lib/screens/clients/search_clients.dart index 895f458..d8e1104 100644 --- a/lib/screens/clients/search_clients.dart +++ b/lib/screens/clients/search_clients.dart @@ -7,11 +7,12 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/clients/client/remove_client_modal.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/widgets/options_menu.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/models/menu_option.dart'; import 'package:adguard_home_manager/classes/process_modal.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; @@ -151,15 +152,15 @@ class _SearchClientsState extends State { ); } - void openOptionsModal(Client client) { - showModal( - context: context, - builder: (ctx) => OptionsModal( - onDelete: () => openDeleteModal(client), - onEdit: () => openClientModal(client), - ) - ); - } + // void openOptionsModal(Client client) { + // showModal( + // context: context, + // builder: (ctx) => OptionsModal( + // onDelete: () => openDeleteModal(client), + // onEdit: () => openClientModal(client), + // ) + // ); + // } return Scaffold( appBar: AppBar( @@ -224,82 +225,96 @@ class _SearchClientsState extends State { primary: false, itemCount: clientsScreen.length, padding: const EdgeInsets.only(bottom: 0), - itemBuilder: (context, index) => ListTile( - contentPadding: index == 0 - ? const EdgeInsets.only(left: 20, right: 20, bottom: 15) - : const EdgeInsets.symmetric(horizontal: 20, vertical: 15), - isThreeLine: true, - onLongPress: () => openOptionsModal(clientsScreen[index]), - onTap: statusProvider.serverStatus != null - ? () => openClientModal(clientsScreen[index]) - : null, - title: Padding( - padding: const EdgeInsets.only(bottom: 5), - child: Text( - clientsScreen[index].name, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.normal + itemBuilder: (context, index) => OptionsMenu( + options: [ + MenuOption( + icon: Icons.edit_rounded, + title: AppLocalizations.of(context)!.edit, + action: (v) => openClientModal(v) + ), + MenuOption( + icon: Icons.delete_rounded, + title: AppLocalizations.of(context)!.delete, + action: (v) => openDeleteModal(v) + ), + ], + value: clientsScreen[index], + child: ListTile( + contentPadding: index == 0 + ? const EdgeInsets.only(left: 20, right: 20, bottom: 15) + : const EdgeInsets.symmetric(horizontal: 20, vertical: 15), + isThreeLine: true, + onTap: statusProvider.serverStatus != null + ? () => openClientModal(clientsScreen[index]) + : null, + title: Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Text( + clientsScreen[index].name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.normal + ), ), ), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(clientsScreen[index].ids.toString().replaceAll(RegExp(r'^\[|\]$'), '')), - const SizedBox(height: 7), - Row( - children: [ - Icon( - Icons.filter_list_rounded, - size: 19, - color: clientsScreen[index].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: clientsScreen[index].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: clientsScreen[index].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: clientsScreen[index].safeSearch != null && clientsScreen[index].safeSearch!.enabled == true - ? appConfigProvider.useThemeColorForStatus == true - ? Theme.of(context).colorScheme.primary - : Colors.green - : appConfigProvider.useThemeColorForStatus == true - ? Colors.grey - : Colors.red - ) - ], - ) - ], + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(clientsScreen[index].ids.toString().replaceAll(RegExp(r'^\[|\]$'), '')), + const SizedBox(height: 7), + Row( + children: [ + Icon( + Icons.filter_list_rounded, + size: 19, + color: clientsScreen[index].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: clientsScreen[index].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: clientsScreen[index].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: clientsScreen[index].safeSearch != null && clientsScreen[index].safeSearch!.enabled == true + ? appConfigProvider.useThemeColorForStatus == true + ? Theme.of(context).colorScheme.primary + : Colors.green + : appConfigProvider.useThemeColorForStatus == true + ? Colors.grey + : Colors.red + ) + ], + ) + ], + ), ), ) ) diff --git a/lib/screens/filters/filters_list.dart b/lib/screens/filters/filters_list.dart index 5c93fb0..f4cca03 100644 --- a/lib/screens/filters/filters_list.dart +++ b/lib/screens/filters/filters_list.dart @@ -172,7 +172,7 @@ class _FiltersListState extends State { loadStatus: widget.loadStatus, onRefresh: () async { final result = await filteringProvider.fetchFilters(); - if (result == false) { + if (result == false && mounted) { showSnacbkar( appConfigProvider: appConfigProvider, label: AppLocalizations.of(context)!.errorLoadFilters, diff --git a/lib/screens/filters/filters_triple_column.dart b/lib/screens/filters/filters_triple_column.dart index 6ecdd8f..3401bac 100644 --- a/lib/screens/filters/filters_triple_column.dart +++ b/lib/screens/filters/filters_triple_column.dart @@ -7,10 +7,10 @@ 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/options_menu.dart'; import 'package:adguard_home_manager/screens/filters/add_button.dart'; import 'package:adguard_home_manager/screens/filters/list_options_menu.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/constants/enums.dart'; import 'package:adguard_home_manager/functions/desktop_mode.dart'; @@ -28,11 +28,11 @@ class FiltersTripleColumn extends StatelessWidget { final List actions; const FiltersTripleColumn({ - Key? key, + super.key, required this.onRemoveCustomRule, required this.onOpenDetailsModal, required this.actions, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -282,28 +282,25 @@ class FiltersTripleColumn extends StatelessWidget { } ), ], - child: CustomListTile( - onLongPress: () => showDialog( - context: context, - builder: (context) => OptionsModal( - options: [ - MenuOption( - title: AppLocalizations.of(context)!.copyClipboard, - icon: Icons.copy_rounded, - action: () => copyToClipboard( - value: filteringProvider.filtering!.userRules[index], - successMessage: AppLocalizations.of(context)!.copiedClipboard, - ) - ) - ] + child: OptionsMenu( + options: [ + MenuOption( + title: AppLocalizations.of(context)!.copyClipboard, + icon: Icons.copy_rounded, + action: (_) => copyToClipboard( + value: filteringProvider.filtering!.userRules[index], + successMessage: AppLocalizations.of(context)!.copiedClipboard, + ) ) - ), - title: filteringProvider.filtering!.userRules[index], - subtitleWidget: generateSubtitle(filteringProvider.filtering!.userRules[index]), - trailing: IconButton( - onPressed: () => onRemoveCustomRule(filteringProvider.filtering!.userRules[index]), - icon: const Icon(Icons.delete), - tooltip: AppLocalizations.of(context)!.delete, + ], + child: CustomListTile( + title: filteringProvider.filtering!.userRules[index], + subtitleWidget: generateSubtitle(filteringProvider.filtering!.userRules[index]), + trailing: IconButton( + onPressed: () => onRemoveCustomRule(filteringProvider.filtering!.userRules[index]), + icon: const Icon(Icons.delete), + tooltip: AppLocalizations.of(context)!.delete, + ), ), ), ), diff --git a/lib/screens/filters/list_options_menu.dart b/lib/screens/filters/list_options_menu.dart index 2e47d36..4cb6865 100644 --- a/lib/screens/filters/list_options_menu.dart +++ b/lib/screens/filters/list_options_menu.dart @@ -8,7 +8,7 @@ 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/widgets/options_modal.dart'; +import 'package:adguard_home_manager/widgets/options_menu.dart'; import 'package:adguard_home_manager/screens/filters/selection/selection_screen.dart'; import 'package:adguard_home_manager/functions/open_url.dart'; @@ -144,41 +144,41 @@ class ListOptionsMenu extends StatelessWidget { child: Material( color: Colors.transparent, child: InkWell( - onLongPress: Platform.isAndroid || Platform.isIOS ? () => showDialog( - context: context, - builder: (context) => OptionsModal( - options: [ - MenuOption( - title: list.enabled == true - ? AppLocalizations.of(context)!.disable - : AppLocalizations.of(context)!.enable, - icon: list.enabled == true - ? Icons.gpp_bad_rounded - : Icons.verified_user_rounded, - action: enableDisable - ), - MenuOption( - title: AppLocalizations.of(context)!.copyListUrl, - icon: Icons.copy_rounded, - action: () => copyToClipboard( - value: list.url, - successMessage: AppLocalizations.of(context)!.listUrlCopied - ) - ), - MenuOption( - title: AppLocalizations.of(context)!.openListUrl, - icon: Icons.open_in_browser_rounded, - action: () => openUrl(list.url) - ), - MenuOption( - title: AppLocalizations.of(context)!.selectionMode, - icon: Icons.check_rounded, - action: openSelectionMode - ), - ] - ) - ) : null, - child: child + child: OptionsMenu( + options: [ + MenuOption( + title: list.enabled == true + ? AppLocalizations.of(context)!.disable + : AppLocalizations.of(context)!.enable, + icon: list.enabled == true + ? Icons.gpp_bad_rounded + : Icons.verified_user_rounded, + action: (_) => enableDisable() + ), + MenuOption( + title: AppLocalizations.of(context)!.copyListUrl, + icon: Icons.copy_rounded, + action: (_) => copyToClipboard( + value: list.url, + successMessage: AppLocalizations.of(context)!.listUrlCopied + ) + ), + MenuOption( + title: AppLocalizations.of(context)!.openListUrl, + icon: Icons.open_in_browser_rounded, + action: (_) => openUrl(list.url) + ), + MenuOption( + title: AppLocalizations.of(context)!.selectionMode, + icon: Icons.check_rounded, + action: (_) => Future.delayed( + const Duration(milliseconds: 0), + () => openSelectionMode() + ) + ), + ], + child: child + ) ), ), ); diff --git a/lib/screens/home/home.dart b/lib/screens/home/home.dart index 343d065..9e5de00 100644 --- a/lib/screens/home/home.dart +++ b/lib/screens/home/home.dart @@ -8,10 +8,10 @@ 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/top_items/top_items_lists.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/top_items.dart'; import 'package:adguard_home_manager/screens/home/chart.dart'; import 'package:adguard_home_manager/functions/number_format.dart'; @@ -21,7 +21,7 @@ import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; class Home extends StatefulWidget { - const Home({Key? key}) : super(key: key); + const Home({super.key}); @override State createState() => _HomeState(); @@ -32,8 +32,11 @@ class _HomeState extends State { late bool isVisible; @override - initState(){ - Provider.of(context, listen: false).getServerStatus(); + initState() { + final statusProvider = Provider.of(context, listen: false); + statusProvider.getServerStatus( + withLoadingIndicator: statusProvider.serverStatus != null ? false : true + ); super.initState(); @@ -233,75 +236,4 @@ class _HomeState extends State { ), ); } -} - -class TopItemsLists extends StatelessWidget { - final List order; - - const TopItemsLists({ - Key? key, - required this.order, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final statusProvider = Provider.of(context); - - List bottom = [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Divider( - thickness: 1, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), - ), - ), - const SizedBox(height: 16), - ]; - - return Column( - children: order.asMap().entries.map((item) { - switch (item.value) { - case HomeTopItems.queriedDomains: - return Column( - children: [ - TopItems( - label: AppLocalizations.of(context)!.topQueriedDomains, - data: statusProvider.serverStatus!.stats.topQueriedDomains, - type: 'topQueriedDomains', - ), - if (item.key < order.length - 1) ...bottom - ], - ); - - case HomeTopItems.blockedDomains: - return Column( - children: [ - TopItems( - label: AppLocalizations.of(context)!.topBlockedDomains, - data: statusProvider.serverStatus!.stats.topBlockedDomains, - type: 'topBlockedDomains', - ), - if (item.key < order.length - 1) ...bottom - ], - ); - - case HomeTopItems.recurrentClients: - return Column( - children: [ - TopItems( - label: AppLocalizations.of(context)!.topClients, - data: statusProvider.serverStatus!.stats.topClients, - type: 'topClients', - clients: true, - ), - if (item.key < order.length - 1) ...bottom - ], - ); - - default: - return const SizedBox(); - } - }).toList(), - ); - } } \ No newline at end of file diff --git a/lib/screens/home/top_items/row_item.dart b/lib/screens/home/top_items/row_item.dart index dbbd6d4..569d30c 100644 --- a/lib/screens/home/top_items/row_item.dart +++ b/lib/screens/home/top_items/row_item.dart @@ -2,30 +2,35 @@ 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/domain_options.dart'; +import 'package:adguard_home_manager/widgets/options_menu.dart'; -import 'package:adguard_home_manager/models/applied_filters.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; -import 'package:adguard_home_manager/providers/logs_provider.dart'; +import 'package:adguard_home_manager/models/menu_option.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/providers/status_provider.dart'; class RowItem extends StatefulWidget { - final String type; + final HomeTopItems type; final Color chartColor; final String domain; final String number; final bool clients; final bool showColor; + final String? unit; + final List options; + final void Function(dynamic)? onTapEntry; const RowItem({ - Key? key, + super.key, required this.type, required this.chartColor, required this.domain, required this.number, required this.clients, required this.showColor, - }) : super(key: key); + required this.options, + this.onTapEntry, + this.unit, + }); @override State createState() => _RowItemState(); @@ -77,8 +82,6 @@ class _RowItemState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { final statusProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - final logsProvider = Provider.of(context); String? name; if (widget.clients == true) { @@ -91,36 +94,10 @@ class _RowItemState extends State with TickerProviderStateMixin { return Material( color: Colors.transparent, - child: DomainOptions( - item: widget.domain, - isClient: widget.type == 'topClients', - isBlocked: widget.type == 'topBlockedDomains', - onTap: () { - if (widget.type == 'topQueriedDomains' || widget.type == 'topBlockedDomains') { - logsProvider.setSearchText(widget.domain); - logsProvider.setSelectedClients(null); - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - searchText: widget.domain, - clients: null - ) - ); - appConfigProvider.setSelectedScreen(2); - } - else if (widget.type == 'topClients') { - logsProvider.setSearchText(null); - logsProvider.setSelectedClients([widget.domain]); - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - searchText: null, - clients: [widget.domain] - ) - ); - appConfigProvider.setSelectedScreen(2); - } - }, + child: OptionsMenu( + value: widget.domain, + options: widget.options, + onTap: widget.onTapEntry, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 20, @@ -195,10 +172,10 @@ class OthersRowItem extends StatefulWidget { final bool showColor; const OthersRowItem({ - Key? key, + super.key, required this.items, required this.showColor, - }) : super(key: key); + }); @override State createState() => _OthersRowItemState(); diff --git a/lib/screens/home/top_items/top_items.dart b/lib/screens/home/top_items/top_items.dart index 5fd6435..00fa63c 100644 --- a/lib/screens/home/top_items/top_items.dart +++ b/lib/screens/home/top_items/top_items.dart @@ -7,26 +7,35 @@ import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/screens/home/top_items/row_item.dart'; -import 'package:adguard_home_manager/widgets/custom_pie_chart.dart'; import 'package:adguard_home_manager/screens/top_items/top_items_modal.dart'; import 'package:adguard_home_manager/screens/top_items/top_items.dart'; +import 'package:adguard_home_manager/widgets/custom_pie_chart.dart'; -import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/models/menu_option.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; class TopItems extends StatefulWidget { - final String type; + final HomeTopItems type; final String label; final List> data; - final bool? clients; + final bool withChart; + final bool withProgressBar; + final String Function(dynamic) buildValue; + final List menuOptions; + final void Function(dynamic)? onTapEntry; const TopItems({ - Key? key, + super.key, required this.type, required this.label, required this.data, - this.clients - }) : super(key: key); + required this.withChart, + required this.withProgressBar, + required this.buildValue, + required this.menuOptions, + this.onTapEntry, + }); @override State createState() => _TopItemsState(); @@ -52,25 +61,9 @@ class _TopItemsState extends State { @override Widget build(BuildContext context) { - final statusProvider = Provider.of(context); - final width = MediaQuery.of(context).size.width; - List> generateData() { - switch (widget.type) { - case 'topQueriedDomains': - return statusProvider.serverStatus!.stats.topQueriedDomains; - - case 'topBlockedDomains': - return statusProvider.serverStatus!.stats.topBlockedDomains; - - case 'topClients': - return statusProvider.serverStatus!.stats.topClients; - - default: - return []; - } - } + final withChart = widget.type != HomeTopItems.avgUpstreamResponseTime; Map chartData() { Map values = {}; @@ -110,105 +103,183 @@ class _TopItemsState extends State { child: Column( children: [ if (widget.data.isEmpty) noItems, - if (widget.data.isNotEmpty && width > 700) Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - flex: 1, - child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 250 - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: CustomPieChart( - data: chartData(), - colors: colors - ) - ), - ) - ), - Expanded( - flex: 2, - child: Column( - children: [ - ItemsList( - colors: colors, - data: widget.data, - clients: widget.clients, - type: widget.type, - showChart: _showChart - ), - OthersRowItem( - items: widget.data, - showColor: true, - ) - ] - ), - ) - ], - ), - if (widget.data.isNotEmpty && width <= 700) ...[ - ExpansionPanelList( - expandedHeaderPadding: const EdgeInsets.all(0), - elevation: 0, - expansionCallback: (_, isExpanded) => setState(() => _showChart = isExpanded), - animationDuration: const Duration(milliseconds: 250), + if (widget.data.isNotEmpty && width > 700) Padding( + padding: EdgeInsets.only(bottom: withChart == false ? 16 : 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - ExpansionPanel( - headerBuilder: (context, isExpanded) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Row( - mainAxisAlignment: width <= 700 - ? MainAxisAlignment.spaceBetween - : MainAxisAlignment.center, - children: [ - Text( + if (withChart == true) Expanded( + flex: 1, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 250 + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: CustomPieChart( + data: chartData(), + colors: colors + ) + ), + ) + ), + Expanded( + flex: 2, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 8, + bottom: 16 + ), + child: Text( widget.label, - style: TextStyle( + style: const TextStyle( fontSize: 18, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface + fontWeight: FontWeight.w500 ), ), - ], - ), + ), + _ItemsList( + colors: colors, + data: widget.data, + clients: widget.type == HomeTopItems.recurrentClients, + type: widget.type, + showChart: withChart == true ? _showChart : false, + buildValue: widget.buildValue, + menuOptions: widget.menuOptions, + onTapEntry: widget.onTapEntry, + ), + if (withChart == true) OthersRowItem( + items: widget.data, + showColor: true, + ) + ] ), - body: Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Column( - children: [ - SizedBox( - height: 150, - child: CustomPieChart( - data: chartData(), - colors: colors - ) - ), - const SizedBox(height: 16), - ], - ), - ), - isExpanded: _showChart - ), + ) ], ), - Padding( - padding: const EdgeInsets.only(top: 8), - child: ItemsList( - colors: colors, - data: widget.data, - clients: widget.clients, - type: widget.type, - showChart: _showChart - ), - ), - OthersRowItem( - items: widget.data, - showColor: _showChart, - ), - const SizedBox(height: 16), - ], + ), + if (widget.data.isNotEmpty && width <= 700) Builder( + builder: (context) { + if (widget.withChart == true) { + return Column( + children: [ + ExpansionPanelList( + expandedHeaderPadding: const EdgeInsets.all(0), + elevation: 0, + expansionCallback: (_, isExpanded) => setState(() => _showChart = isExpanded), + animationDuration: const Duration(milliseconds: 250), + children: [ + ExpansionPanel( + headerBuilder: (context, isExpanded) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: width <= 700 + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + widget.label, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ), + ], + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + children: [ + SizedBox( + height: 150, + child: CustomPieChart( + data: chartData(), + colors: colors + ) + ), + const SizedBox(height: 16), + ], + ), + ), + isExpanded: _showChart + ), + ], + ), + Padding( + padding: const EdgeInsets.only(top: 8), + child: _ItemsList( + colors: colors, + data: widget.data, + clients: widget.type == HomeTopItems.recurrentClients, + type: widget.type, + showChart: _showChart, + buildValue: widget.buildValue, + menuOptions: widget.menuOptions, + onTapEntry: widget.onTapEntry, + ), + ), + if (widget.withChart == true) OthersRowItem( + items: widget.data, + showColor: _showChart, + ), + const SizedBox(height: 16), + ], + ); + } + else { + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18), + child: Row( + mainAxisAlignment: width <= 700 + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + widget.label, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface + ), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16), + child: _ItemsList( + colors: colors, + data: widget.data, + clients: widget.type == HomeTopItems.recurrentClients, + type: widget.type, + showChart: false, + buildValue: widget.buildValue, + menuOptions: widget.menuOptions, + onTapEntry: widget.onTapEntry, + ), + ), + if (widget.withChart == true) OthersRowItem( + items: widget.data, + showColor: false, + ), + const SizedBox(height: 16), + ], + ); + } + }, + ), if (widget.data.length > 5) ...[ Padding( @@ -225,8 +296,12 @@ class _TopItemsState extends State { builder: (context) => TopItemsModal( type: widget.type, title: widget.label, - isClient: widget.clients, - data: generateData(), + isClient: widget.type == HomeTopItems.recurrentClients, + data: widget.data, + withProgressBar: widget.withProgressBar, + buildValue: widget.buildValue, + options: widget.menuOptions, + onTapEntry: widget.onTapEntry, ) ) } @@ -236,8 +311,12 @@ class _TopItemsState extends State { builder: (context) => TopItemsScreen( type: widget.type, title: widget.label, - isClient: widget.clients, - data: generateData(), + isClient: widget.type == HomeTopItems.recurrentClients, + data: widget.data, + withProgressBar: widget.withProgressBar, + buildValue: widget.buildValue, + menuOptions: widget.menuOptions, + onTapEntry: widget.onTapEntry, ) ) ) @@ -266,21 +345,26 @@ class _TopItemsState extends State { } } -class ItemsList extends StatelessWidget { +class _ItemsList extends StatelessWidget { final List colors; final List> data; final bool? clients; - final String type; + final HomeTopItems type; final bool showChart; + final String Function(dynamic) buildValue; + final List menuOptions; + final void Function(dynamic)? onTapEntry; - const ItemsList({ - Key? key, + const _ItemsList({ required this.colors, required this.data, required this.clients, required this.type, required this.showChart, - }) : super(key: key); + required this.buildValue, + required this.menuOptions, + this.onTapEntry, + }); @override Widget build(BuildContext context) { @@ -290,10 +374,12 @@ class ItemsList extends StatelessWidget { ).asMap().entries.map((e) => RowItem( clients: clients ?? false, domain: e.value.keys.toList()[0], - number: e.value.values.toList()[0].toString(), + number: buildValue(e.value.values.toList()[0]), type: type, chartColor: colors[e.key], showColor: showChart, + options: menuOptions, + onTapEntry: onTapEntry, )).toList() ); } diff --git a/lib/screens/home/top_items/top_items_lists.dart b/lib/screens/home/top_items/top_items_lists.dart new file mode 100644 index 0000000..2f8820d --- /dev/null +++ b/lib/screens/home/top_items/top_items_lists.dart @@ -0,0 +1,239 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/screens/home/top_items/top_items.dart'; + +import 'package:adguard_home_manager/functions/number_format.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/models/applied_filters.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; +import 'package:adguard_home_manager/providers/logs_provider.dart'; +import 'package:adguard_home_manager/functions/copy_clipboard.dart'; +import 'package:adguard_home_manager/models/menu_option.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; + +class TopItemsLists extends StatelessWidget { + final List order; + + const TopItemsLists({ + super.key, + required this.order, + }); + + @override + Widget build(BuildContext context) { + final statusProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + final logsProvider = Provider.of(context); + + List bottom = [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Divider( + thickness: 1, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), + ), + ), + const SizedBox(height: 16), + ]; + + void filterDomainLogs({required String value}) { + logsProvider.setSearchText(value); + logsProvider.setSelectedClients(null); + logsProvider.setAppliedFilters( + AppliedFiters( + selectedResultStatus: 'all', + searchText: value, + clients: null + ) + ); + appConfigProvider.setSelectedScreen(2); + } + + void filterClientLogs({required String value}) { + logsProvider.setSearchText(null); + logsProvider.setSelectedClients([value]); + logsProvider.setAppliedFilters( + AppliedFiters( + selectedResultStatus: 'all', + searchText: null, + clients: [value] + ) + ); + appConfigProvider.setSelectedScreen(2); + } + + void blockUnblock({required String domain, required String newStatus}) async { + final ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.savingUserFilters); + + final rules = await statusProvider.blockUnblockDomain( + domain: domain, + newStatus: newStatus + ); + + processModal.close(); + + if (!context.mounted) return; + if (rules == true) { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.userFilteringRulesUpdated, + color: Colors.green + ); + } + else { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.userFilteringRulesNotUpdated, + color: Colors.red + ); + } + } + + void copyValueClipboard(value) { + copyToClipboard(value: value, successMessage: AppLocalizations.of(context)!.copiedClipboard); + } + + return Column( + children: order.asMap().entries.map((item) { + switch (item.value) { + case HomeTopItems.queriedDomains: + return Column( + children: [ + TopItems( + label: AppLocalizations.of(context)!.topQueriedDomains, + type: HomeTopItems.queriedDomains, + data: statusProvider.serverStatus?.stats.topQueriedDomains ?? [], + withChart: true, + withProgressBar: true, + buildValue: (v) => v.toString(), + menuOptions: [ + MenuOption( + title: AppLocalizations.of(context)!.blockDomain, + icon: Icons.block_rounded, + action: (v) => blockUnblock(domain: v.toString(), newStatus: 'block') + ), + MenuOption( + title: AppLocalizations.of(context)!.copyClipboard, + icon: Icons.copy_rounded, + action: copyValueClipboard + ), + ], + onTapEntry: (v) => filterDomainLogs(value: v.toString()), + ), + if (item.key < order.length - 1) ...bottom + ], + ); + + case HomeTopItems.blockedDomains: + return Column( + children: [ + TopItems( + label: AppLocalizations.of(context)!.topBlockedDomains, + type: HomeTopItems.blockedDomains, + data: statusProvider.serverStatus?.stats.topBlockedDomains ?? [], + withChart: true, + withProgressBar: true, + buildValue: (v) => v.toString(), + menuOptions: [ + MenuOption( + title: AppLocalizations.of(context)!.unblockDomain, + icon: Icons.check_rounded, + action: (v) => blockUnblock(domain: v, newStatus: 'unblock') + ), + MenuOption( + title: AppLocalizations.of(context)!.copyClipboard, + icon: Icons.copy_rounded, + action: copyValueClipboard + ) + ], + onTapEntry: (v) => filterDomainLogs(value: v), + ), + if (item.key < order.length - 1) ...bottom + ], + ); + + case HomeTopItems.recurrentClients: + return Column( + children: [ + TopItems( + label: AppLocalizations.of(context)!.topClients, + type: HomeTopItems.recurrentClients, + data: statusProvider.serverStatus?.stats.topClients ?? [], + withChart: true, + withProgressBar: true, + buildValue: (v) => v.toString(), + menuOptions: [ + MenuOption( + title: AppLocalizations.of(context)!.copyClipboard, + icon: Icons.copy_rounded, + action: copyValueClipboard + ) + ], + onTapEntry: (v) => filterClientLogs(value: v), + ), + if (item.key < order.length - 1) ...bottom + ], + ); + + case HomeTopItems.topUpstreams: + return statusProvider.serverStatus!.stats.topUpstreamResponses != null + ? Column( + children: [ + TopItems( + label: AppLocalizations.of(context)!.topUpstreams, + type: HomeTopItems.topUpstreams, + data: statusProvider.serverStatus?.stats.topUpstreamResponses ?? [], + withChart: true, + withProgressBar: true, + buildValue: (v) => v.toString(), + menuOptions: [ + MenuOption( + title: AppLocalizations.of(context)!.copyClipboard, + icon: Icons.copy_rounded, + action: copyValueClipboard + ) + ], + ), + if (item.key < order.length - 1) ...bottom + ], + ) + : const SizedBox(); + + case HomeTopItems.avgUpstreamResponseTime: + return statusProvider.serverStatus!.stats.topUpstreamsAvgTime != null + ? Column( + children: [ + TopItems( + label: AppLocalizations.of(context)!.averageUpstreamResponseTime, + type: HomeTopItems.avgUpstreamResponseTime, + data: statusProvider.serverStatus?.stats.topUpstreamsAvgTime ?? [], + withChart: false, + withProgressBar: false, + buildValue: (v) => "${doubleFormat(v*1000, Platform.localeName)} ms", + menuOptions: [ + MenuOption( + title: AppLocalizations.of(context)!.copyClipboard, + icon: Icons.copy_rounded, + action: copyValueClipboard + ) + ], + ), + if (item.key < order.length - 1) ...bottom + ], + ) + : const SizedBox(); + + default: + return const SizedBox(); + } + }).toList(), + ); + } +} \ No newline at end of file diff --git a/lib/screens/logs/log_tile.dart b/lib/screens/logs/log_tile.dart index 14c5262..95981e8 100644 --- a/lib/screens/logs/log_tile.dart +++ b/lib/screens/logs/log_tile.dart @@ -2,9 +2,15 @@ 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/domain_options.dart'; +import 'package:adguard_home_manager/widgets/options_menu.dart'; +import 'package:adguard_home_manager/providers/status_provider.dart'; +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/functions/copy_clipboard.dart'; +import 'package:adguard_home_manager/models/menu_option.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; import 'package:adguard_home_manager/functions/get_filtered_status.dart'; import 'package:adguard_home_manager/models/logs.dart'; @@ -20,7 +26,7 @@ class LogTile extends StatelessWidget { final bool twoColumns; const LogTile({ - Key? key, + super.key, required this.log, required this.length, required this.index, @@ -28,11 +34,12 @@ class LogTile extends StatelessWidget { required this.onLogTap, this.useAlwaysNormalTile, required this.twoColumns, - }) : super(key: key); + }); @override Widget build(BuildContext context) { final appConfigProvider = Provider.of(context); + final statusProvider = Provider.of(context); Widget logStatusWidget({ required IconData icon, @@ -83,16 +90,63 @@ class LogTile extends StatelessWidget { } } + void blockUnblock({required String domain, required String newStatus}) async { + final ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.savingUserFilters); + + final rules = await statusProvider.blockUnblockDomain( + domain: domain, + newStatus: newStatus + ); + + processModal.close(); + + if (!context.mounted) return; + if (rules == true) { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.userFilteringRulesUpdated, + color: Colors.green + ); + } + else { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.userFilteringRulesNotUpdated, + color: Colors.red + ); + } + } + + final domainBlocked = isDomainBlocked(log.reason); + if (twoColumns && !(useAlwaysNormalTile == true)) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: InkWell( borderRadius: BorderRadius.circular(28), - child: DomainOptions( - onTap: () => onLogTap(log), + child: OptionsMenu( + onTap: (_) => onLogTap(log), borderRadius: BorderRadius.circular(28), - item: log.question.name, - isBlocked: isDomainBlocked(log.reason), + options: [ + if (log.question.name != null) MenuOption( + title: domainBlocked == true + ? AppLocalizations.of(context)!.unblockDomain + : AppLocalizations.of(context)!.blockDomain, + icon: domainBlocked == true + ? Icons.check_rounded + : Icons.block_rounded, + action: (_) => blockUnblock( + domain: log.question.name!, + newStatus: domainBlocked == true ? 'unblock' : 'block' + ) + ), + MenuOption( + title: AppLocalizations.of(context)!.copyClipboard, + icon: Icons.copy_rounded, + action: (v) => copyToClipboard(value: v, successMessage: AppLocalizations.of(context)!.copiedClipboard) + ) + ], child: Container( width: double.maxFinite, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), @@ -250,10 +304,27 @@ class LogTile extends StatelessWidget { else { return Material( color: Colors.transparent, - child: DomainOptions( - onTap: () => onLogTap(log), - item: log.question.name, - isBlocked: isDomainBlocked(log.reason), + child: OptionsMenu( + onTap: (_) => onLogTap(log), + options: [ + if (log.question.name != null) MenuOption( + title: domainBlocked == true + ? AppLocalizations.of(context)!.unblockDomain + : AppLocalizations.of(context)!.blockDomain, + icon: domainBlocked == true + ? Icons.check_rounded + : Icons.block_rounded, + action: (_) => blockUnblock( + domain: log.question.name!, + newStatus: domainBlocked == true ? 'unblock' : 'block' + ) + ), + MenuOption( + title: AppLocalizations.of(context)!.copyClipboard, + icon: Icons.copy_rounded, + action: (v) => copyToClipboard(value: v, successMessage: AppLocalizations.of(context)!.copiedClipboard) + ) + ], child: Container( width: double.maxFinite, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), diff --git a/lib/screens/logs/logs.dart b/lib/screens/logs/logs.dart index c1510c3..757c949 100644 --- a/lib/screens/logs/logs.dart +++ b/lib/screens/logs/logs.dart @@ -1,11 +1,12 @@ // ignore_for_file: use_build_context_synchronously -import 'package:adguard_home_manager/models/logs.dart'; import 'package:flutter/material.dart'; import 'package:adguard_home_manager/screens/logs/logs_list.dart'; import 'package:adguard_home_manager/screens/logs/details/log_details_screen.dart'; +import 'package:adguard_home_manager/models/logs.dart'; + class Logs extends StatefulWidget { const Logs({Key? key}) : super(key: key); diff --git a/lib/screens/settings/encryption/encryption.dart b/lib/screens/settings/encryption/encryption.dart index a618dbe..f47a63d 100644 --- a/lib/screens/settings/encryption/encryption.dart +++ b/lib/screens/settings/encryption/encryption.dart @@ -72,7 +72,8 @@ class _EncryptionSettingsState extends State { String? validDataError; int certKeyValidApi = 0; - EncyptionValidation? certKeyValid; + EncryptionValidation? certKeyValid; + String? encryptionResultMessage; bool formEdited = false; @@ -140,20 +141,27 @@ class _EncryptionSettingsState extends State { if (!mounted) return; if (result.successful == true) { - final data = result.content as EncyptionValidation; - setState(() { - if (data.warningValidation != null && data.warningValidation != '') { + final data = result.content as EncryptionValidationResult; + if (data.isObject == true) { + final object = data.encryptionValidation!; + setState(() { + if (object.warningValidation != null && object.warningValidation != '') { + certKeyValidApi = 2; + validDataError = object.warningValidation; + } + else { + certKeyValidApi = 1; + validDataError = null; + } + certKeyValid = object; + }); + } + else { + setState(() { + encryptionResultMessage = data.message; certKeyValidApi = 2; - validDataError = data.warningValidation; - } - else { - certKeyValidApi = 1; - validDataError = null; - } - certKeyValid = data; - }); - } - else { + }); + } } } @@ -253,11 +261,13 @@ class _EncryptionSettingsState extends State { centerTitle: false, actions: [ IconButton( - onPressed: certKeyValidApi == 2 && validDataError != null + onPressed: certKeyValidApi == 2 && (validDataError != null || encryptionResultMessage != null) ? () => { showDialog( context: context, - builder: (context) => EncryptionErrorModal(error: validDataError!) + builder: (context) => EncryptionErrorModal( + error: validDataError ?? encryptionResultMessage ?? AppLocalizations.of(context)!.unknownError + ) ) } : null, icon: generateStatus(context, appConfigProvider, localValidationValid, certKeyValidApi, formEdited), diff --git a/lib/screens/settings/general_settings/reorderable_top_items_home.dart b/lib/screens/settings/general_settings/reorderable_top_items_home.dart index b649ff5..2b7bfe1 100644 --- a/lib/screens/settings/general_settings/reorderable_top_items_home.dart +++ b/lib/screens/settings/general_settings/reorderable_top_items_home.dart @@ -1,6 +1,5 @@ // ignore_for_file: use_build_context_synchronously -import 'package:adguard_home_manager/functions/desktop_mode.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -9,15 +8,16 @@ 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/desktop_mode.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; -class ItemData { +class _ItemData { final HomeTopItems title; final Key key; - const ItemData({ + const _ItemData({ required this.title, required this.key }); @@ -29,7 +29,7 @@ enum DraggingMode { } class ReorderableTopItemsHome extends StatefulWidget { - const ReorderableTopItemsHome({Key? key}) : super(key: key); + const ReorderableTopItemsHome({super.key}); @override State createState() => _ReorderableTopItemsHomeState(); @@ -38,10 +38,10 @@ class ReorderableTopItemsHome extends StatefulWidget { class _ReorderableTopItemsHomeState extends State { List homeTopItemsList = []; List persistHomeTopItemsList = []; - List renderItems = []; + List<_ItemData> renderItems = []; int _indexOfKey(Key key) { - return renderItems.indexWhere((ItemData d) => d.key == key); + return renderItems.indexWhere((_ItemData d) => d.key == key); } bool _reorderCallback(Key item, Key newPosition) { @@ -79,7 +79,7 @@ class _ReorderableTopItemsHomeState extends State { homeTopItemsList = appConfigProvider.homeTopItemsOrder; persistHomeTopItemsList = appConfigProvider.homeTopItemsOrder; renderItems = appConfigProvider.homeTopItemsOrder.asMap().entries.map( - (e) => ItemData( + (e) => _ItemData( key: ValueKey(e.key), title: e.value, ) @@ -117,16 +117,31 @@ class _ReorderableTopItemsHomeState extends State { padding: const EdgeInsets.all(16) ); + case HomeTopItems.topUpstreams: + return CustomListTile( + title: AppLocalizations.of(context)!.topUpstreams, + icon: Icons.upload_file_rounded, + padding: const EdgeInsets.all(16) + ); + + case HomeTopItems.avgUpstreamResponseTime: + return CustomListTile( + title: AppLocalizations.of(context)!.averageUpstreamResponseTime, + icon: Icons.timer_rounded, + padding: const EdgeInsets.all(16) + ); + default: return const SizedBox(); } } - Future onWillPopScope() async { + Future onWillPopScope(bool popInvoked) async { if (!listEquals(appConfigProvider.homeTopItemsOrder, persistHomeTopItemsList)) { - showDialog( + await showDialog( context: context, - builder: (dialogContext) => AlertDialog( + useRootNavigator: false, + builder: (ctx) => AlertDialog( title: Text(AppLocalizations.of(context)!.discardChanges), content: Text(AppLocalizations.of(context)!.discardChangesDescription), actions: [ @@ -135,14 +150,14 @@ class _ReorderableTopItemsHomeState extends State { children: [ TextButton( onPressed: () { - Navigator.pop(dialogContext); + Navigator.pop(context); Navigator.pop(context); }, child: Text(AppLocalizations.of(context)!.confirm) ), const SizedBox(width: 8), TextButton( - onPressed: () => Navigator.pop(dialogContext), + onPressed: () => Navigator.pop(context), child: Text(AppLocalizations.of(context)!.cancel) ), ], @@ -175,8 +190,9 @@ class _ReorderableTopItemsHomeState extends State { } } - return WillPopScope( - onWillPop: onWillPopScope, + return PopScope( + canPop: listEquals(appConfigProvider.homeTopItemsOrder, persistHomeTopItemsList), + onPopInvoked: onWillPopScope, child: Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.topItemsOrder), @@ -219,7 +235,7 @@ class _ReorderableTopItemsHomeState extends State { child: ListView.builder( itemBuilder: (context, index) => reorderable_list_library.ReorderableItem( key: renderItems[index].key, - childBuilder: (context, state) => Item( + childBuilder: (context, state) => _Item( tileWidget: tile(renderItems[index].title), isFirst: index == 0, isLast: index == renderItems.length - 1, @@ -237,19 +253,18 @@ class _ReorderableTopItemsHomeState extends State { } } -class Item extends StatelessWidget { +class _Item extends StatelessWidget { final Widget tileWidget; final bool isFirst; final bool isLast; final reorderable_list_library.ReorderableItemState state; - const Item({ - Key? key, + const _Item({ required this.tileWidget, required this.isFirst, required this.isLast, required this.state, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/screens/top_items/top_items.dart b/lib/screens/top_items/top_items.dart index 26b84d5..498ce75 100644 --- a/lib/screens/top_items/top_items.dart +++ b/lib/screens/top_items/top_items.dart @@ -7,29 +7,37 @@ import 'package:percent_indicator/percent_indicator.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/widgets/domain_options.dart'; +import 'package:adguard_home_manager/widgets/options_menu.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; -import 'package:adguard_home_manager/models/applied_filters.dart'; -import 'package:adguard_home_manager/providers/logs_provider.dart'; +import 'package:adguard_home_manager/models/menu_option.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/providers/status_provider.dart'; import 'package:adguard_home_manager/functions/snackbar.dart'; import 'package:adguard_home_manager/functions/number_format.dart'; import 'package:adguard_home_manager/providers/app_config_provider.dart'; class TopItemsScreen extends StatefulWidget { - final String type; + final HomeTopItems type; final String title; final bool? isClient; final List> data; + final bool withProgressBar; + final String Function(dynamic) buildValue; + final List menuOptions; + final void Function(dynamic)? onTapEntry; const TopItemsScreen({ - Key? key, + super.key, required this.type, required this.title, this.isClient, required this.data, - }) : super(key: key); + required this.withProgressBar, + required this.buildValue, + required this.menuOptions, + this.onTapEntry, + }); @override State createState() => _TopItemsScreenState(); @@ -58,11 +66,10 @@ class _TopItemsScreenState extends State { Widget build(BuildContext context) { final statusProvider = Provider.of(context); final appConfigProvider = Provider.of(context); - final logsProvider = Provider.of(context); - int total = 0; + double total = 0; for (var element in data) { - total = total + int.parse(element.values.toList()[0].toString()); + total = total + double.parse(element.values.toList()[0].toString()); } return Scaffold( @@ -152,42 +159,19 @@ class _TopItemsScreenState extends State { } } - return DomainOptions( - item: screenData[index].keys.toList()[0], - isBlocked: widget.type == 'topBlockedDomains', - isClient: widget.type == 'topClients', - onTap: () { - if (widget.type == 'topQueriedDomains' || widget.type == 'topBlockedDomains') { - logsProvider.setSearchText(screenData[index].keys.toList()[0]); - logsProvider.setSelectedClients(null); - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - searchText: screenData[index].keys.toList()[0], - clients: null - ) - ); - appConfigProvider.setSelectedScreen(2); - Navigator.pop(context); - } - else if (widget.type == 'topClients') { - logsProvider.setSearchText(null); - logsProvider.setSelectedClients([screenData[index].keys.toList()[0]]); - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - searchText: null, - clients: [screenData[index].keys.toList()[0]] - ) - ); - appConfigProvider.setSelectedScreen(2); - Navigator.pop(context); - } - }, + return OptionsMenu( + value: screenData[index].keys.toList()[0], + options: widget.menuOptions, + onTap: widget.onTapEntry != null + ? (v) { + widget.onTapEntry!(v); + Navigator.pop(context); + } + : null, child: CustomListTile( title: screenData[index].keys.toList()[0], trailing: Text( - screenData[index].values.toList()[0].toString(), + widget.buildValue(screenData[index].values.toList()[0]), style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant ), @@ -205,7 +189,7 @@ class _TopItemsScreenState extends State { ), const SizedBox(height: 5), ], - Row( + if (widget.withProgressBar == true) Row( children: [ SizedBox( width: 50, diff --git a/lib/screens/top_items/top_items_modal.dart b/lib/screens/top_items/top_items_modal.dart index 845459a..36ea793 100644 --- a/lib/screens/top_items/top_items_modal.dart +++ b/lib/screens/top_items/top_items_modal.dart @@ -7,28 +7,35 @@ import 'package:percent_indicator/percent_indicator.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:adguard_home_manager/widgets/domain_options.dart'; +import 'package:adguard_home_manager/widgets/options_menu.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; -import 'package:adguard_home_manager/models/applied_filters.dart'; -import 'package:adguard_home_manager/providers/logs_provider.dart'; +import 'package:adguard_home_manager/models/menu_option.dart'; +import 'package:adguard_home_manager/constants/enums.dart'; import 'package:adguard_home_manager/functions/number_format.dart'; import 'package:adguard_home_manager/providers/status_provider.dart'; -import 'package:adguard_home_manager/providers/app_config_provider.dart'; class TopItemsModal extends StatefulWidget { - final String type; + final HomeTopItems type; final String title; final bool? isClient; final List> data; + final bool withProgressBar; + final String Function(dynamic) buildValue; + final List options; + final void Function(dynamic)? onTapEntry; const TopItemsModal({ - Key? key, + super.key, required this.type, required this.title, this.isClient, required this.data, - }) : super(key: key); + required this.withProgressBar, + required this.buildValue, + required this.options, + this.onTapEntry, + }); @override State createState() => _TopItemsModalState(); @@ -56,12 +63,10 @@ class _TopItemsModalState extends State { @override Widget build(BuildContext context) { final statusProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - final logsProvider = Provider.of(context); - int total = 0; + double total = 0; for (var element in data) { - total = total + int.parse(element.values.toList()[0].toString()); + total = total + double.parse(element.values.toList()[0].toString()); } return Dialog( @@ -128,42 +133,19 @@ class _TopItemsModalState extends State { } } - return DomainOptions( - isBlocked: widget.type == 'topBlockedDomains', - isClient: widget.type == 'topClients', - item: screenData[index].keys.toList()[0], - onTap: () { - if (widget.type == 'topQueriedDomains' || widget.type == 'topBlockedDomains') { - logsProvider.setSearchText(screenData[index].keys.toList()[0]); - logsProvider.setSelectedClients(null); - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - searchText: screenData[index].keys.toList()[0], - clients: null - ) - ); - appConfigProvider.setSelectedScreen(2); - Navigator.pop(context); - } - else if (widget.type == 'topClients') { - logsProvider.setSearchText(null); - logsProvider.setSelectedClients([screenData[index].keys.toList()[0]]); - logsProvider.setAppliedFilters( - AppliedFiters( - selectedResultStatus: 'all', - searchText: null, - clients: [screenData[index].keys.toList()[0]] - ) - ); - appConfigProvider.setSelectedScreen(2); - Navigator.pop(context); - } - }, + return OptionsMenu( + options: widget.options, + value: screenData[index].keys.toList()[0], + onTap: widget.onTapEntry != null + ? (v) { + widget.onTapEntry!(v); + Navigator.pop(context); + } + : null, child: CustomListTile( title: screenData[index].keys.toList()[0], trailing: Text( - screenData[index].values.toList()[0].toString(), + widget.buildValue(screenData[index].values.toList()[0]), style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant ), @@ -181,7 +163,7 @@ class _TopItemsModalState extends State { ), const SizedBox(height: 5), ], - Row( + if (widget.withProgressBar == true) Row( children: [ SizedBox( width: 50, diff --git a/lib/services/api_client.dart b/lib/services/api_client.dart index ba56a9c..66ac615 100644 --- a/lib/services/api_client.dart +++ b/lib/services/api_client.dart @@ -698,9 +698,18 @@ class ApiClientV2 { try { return ApiResponse( successful: result.successful, - content: result.body != null - ? EncyptionValidation.fromJson(jsonDecode(result.body!)) - : null + content: result.body != null ? EncryptionValidationResult( + isObject: false, + encryptionValidation: EncryptionValidation.fromJson(jsonDecode(result.body!)) + ) : null + ); + } on FormatException { + return ApiResponse( + successful: result.successful, + content: result.body != null ? EncryptionValidationResult( + isObject: false, + message: result.body + ) : null ); } catch (e, stackTrace) { Sentry.captureException(e, stackTrace: stackTrace); diff --git a/lib/widgets/custom_list_tile_dialog.dart b/lib/widgets/custom_list_tile_dialog.dart index 1d09760..2e3cff4 100644 --- a/lib/widgets/custom_list_tile_dialog.dart +++ b/lib/widgets/custom_list_tile_dialog.dart @@ -6,11 +6,11 @@ class CustomListTileDialog extends StatelessWidget { final void Function()? onTap; const CustomListTileDialog({ - Key? key, + super.key, required this.title, this.icon, this.onTap - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/domain_options.dart b/lib/widgets/domain_options.dart deleted file mode 100644 index 3c4bb79..0000000 --- a/lib/widgets/domain_options.dart +++ /dev/null @@ -1,136 +0,0 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:flutter/material.dart'; -import 'package:contextmenu/contextmenu.dart'; -import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'package:adguard_home_manager/widgets/options_modal.dart'; -import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; - -import 'package:adguard_home_manager/functions/snackbar.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/models/menu_option.dart'; - -class DomainOptions extends StatelessWidget { - final bool isBlocked; - final bool? isClient; - final String? item; - final Widget child; - final void Function() onTap; - final BorderRadius? borderRadius; - - const DomainOptions({ - Key? key, - required this.isBlocked, - this.isClient, - required this.item, - required this.child, - required this.onTap, - this.borderRadius - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final statusProvider = Provider.of(context); - final appConfigProvider = Provider.of(context); - - void blockUnblock(String domain, String newStatus) async { - final ProcessModal processModal = ProcessModal(); - processModal.open(AppLocalizations.of(context)!.savingUserFilters); - - final rules = await statusProvider.blockUnblockDomain( - domain: domain, - newStatus: newStatus - ); - - processModal.close(); - - if (rules == true) { - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.userFilteringRulesUpdated, - color: Colors.green - ); - } - else { - showSnacbkar( - appConfigProvider: appConfigProvider, - label: AppLocalizations.of(context)!.userFilteringRulesNotUpdated, - color: Colors.red - - ); - } - } - - void copyDomainClipboard(String domain) async { - await Clipboard.setData( - ClipboardData(text: domain) - ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context)!.domainCopiedClipboard), - backgroundColor: Colors.green, - ) - ); - } - - List generateOptions() { - return [ - if (isClient != true && isBlocked == true) MenuOption( - title: AppLocalizations.of(context)!.unblock, - icon: Icons.check, - action: () => blockUnblock(item!, 'unblock') - ), - if (isClient != true && isBlocked == false) MenuOption( - title: AppLocalizations.of(context)!.block, - icon: Icons.block, - action: () => blockUnblock(item!, 'block') - ), - MenuOption( - title: AppLocalizations.of(context)!.copyClipboard, - icon: Icons.copy, - action: () => copyDomainClipboard(item!) - ), - ]; - } - - void openOptionsModal() { - showDialog( - context: context, - builder: (context) => OptionsModal( - options: generateOptions(), - ) - ); - } - - if (item != null) { - return Material( - color: Colors.transparent, - borderRadius: borderRadius, - child: ContextMenuArea( - builder: (context) => generateOptions().map((opt) => CustomListTile( - title: opt.title, - icon: opt.icon, - onTap: () { - opt.action(); - Navigator.pop(context); - }, - )).toList(), - child: InkWell( - onTap: onTap, - onLongPress: openOptionsModal, - borderRadius: borderRadius, - child: child, - ), - ), - ); - } - else { - return child; - } - } -} \ No newline at end of file diff --git a/lib/widgets/options_modal.dart b/lib/widgets/options_menu.dart similarity index 51% rename from lib/widgets/options_modal.dart rename to lib/widgets/options_menu.dart index 75a55a5..861edab 100644 --- a/lib/widgets/options_modal.dart +++ b/lib/widgets/options_menu.dart @@ -1,15 +1,75 @@ -import 'package:adguard_home_manager/models/menu_option.dart'; +import 'dart:io'; + +import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:adguard_home_manager/widgets/custom_list_tile_dialog.dart'; +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; -class OptionsModal extends StatelessWidget { +import 'package:adguard_home_manager/models/menu_option.dart'; + +class OptionsMenu extends StatelessWidget { + final Widget child; final List options; + final dynamic value; + final BorderRadius? borderRadius; + final void Function(dynamic)? onTap; - const OptionsModal({ + const OptionsMenu({ super.key, + required this.child, required this.options, + this.value, + this.borderRadius, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + void openOptionsModal() { + showDialog( + context: context, + builder: (context) => _OptionsModal( + options: options, + value: value + ) + ); + } + + return Material( + color: Colors.transparent, + child: ContextMenuArea( + builder: (context) => options.map((opt) => CustomListTile( + title: opt.title, + icon: opt.icon, + onTap: () { + opt.action(value); + Navigator.pop(context); + }, + )).toList(), + child: InkWell( + onTap: onTap != null + ? () => onTap!(value) + : null, + onLongPress: (Platform.isAndroid || Platform.isIOS) + ? () => openOptionsModal() + : null, + borderRadius: borderRadius, + child: child, + ), + ), + ); + } +} + +class _OptionsModal extends StatelessWidget { + final List options; + final dynamic value; + + const _OptionsModal({ + required this.options, + this.value, }); @override @@ -43,7 +103,7 @@ class OptionsModal extends StatelessWidget { icon: opt.icon, onTap: () { Navigator.pop(context); - opt.action(); + opt.action(value); }, )).toList() ), diff --git a/pubspec.lock b/pubspec.lock index 0582275..274cfb3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.8" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" + url: "https://pub.dev" + source: hosted + version: "2.0.2" archive: dependency: transitive description: @@ -133,10 +141,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "7035152271ff67b072a211152846e9f1259cf1be41e34cd3e0b5463d2d6b8419" + sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "9.1.1" device_info_plus_platform_interface: dependency: transitive description: @@ -197,10 +205,10 @@ packages: dependency: "direct main" description: name: fl_chart - sha256: "6b9eb2b3017241d05c482c01f668dd05cc909ec9a0114fdd49acd958ff2432fa" + sha256: "5a74434cc83bf64346efb562f1a06eefaf1bcb530dc3d96a104f631a1eff8d79" url: "https://pub.dev" source: hosted - version: "0.64.0" + version: "0.65.0" flutter: dependency: "direct main" description: flutter @@ -263,10 +271,10 @@ packages: dependency: "direct dev" description: name: flutter_native_splash - sha256: d93394f22f73e810bda59e11ebe83329c5511d6460b6b7509c4e1f3c92d6d625 + sha256: c4d899312b36e7454bedfd0a4740275837b99e532d81c8477579d8183db1de6c url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.3.6" flutter_reorderable_list: dependency: "direct main" description: @@ -426,10 +434,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" + sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "5.0.1" package_info_plus_platform_interface: dependency: transitive description: @@ -514,18 +522,18 @@ packages: dependency: transitive description: name: sentry - sha256: e8040183ea1a0323999bce69786ed8429b1b89fbe5815a504d5bb7493a6464cc + sha256: e7ded42974bac5f69e4ca4ddc57d30499dd79381838f24b7e8fd9aa4139e7b79 url: "https://pub.dev" source: hosted - version: "7.13.1" + version: "7.13.2" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: "843e317e605e5860e30ae431e9b8724c54f1a8567c9ad495e04595926bf22376" + sha256: d6f55ec7a1f681784165021f749007712a72ff57eadf91e963331b6ae326f089 url: "https://pub.dev" source: hosted - version: "7.13.1" + version: "7.13.2" sky_engine: dependency: transitive description: flutter @@ -813,5 +821,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0-194.0.dev <4.0.0" + dart: ">=3.2.0 <4.0.0" flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index c52eb16..8770025 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 2.11.3+108 +version: 2.12.0-beta.2+110 environment: sdk: '>=2.18.1 <3.0.0' @@ -41,14 +41,14 @@ dependencies: intl: any provider: ^6.1.1 sqflite: ^2.3.0 - package_info_plus: ^4.2.0 + package_info_plus: ^5.0.1 flutter_displaymode: ^0.6.0 dynamic_color: ^1.6.8 animations: ^2.0.8 - device_info_plus: ^9.1.0 + device_info_plus: ^9.1.1 uuid: ^4.2.1 expandable: ^5.0.1 - fl_chart: ^0.64.0 + fl_chart: ^0.65.0 flutter_web_browser: ^0.17.1 flutter_svg: ^2.0.9 percent_indicator: ^4.2.3 @@ -70,7 +70,7 @@ dependencies: url_launcher: ^6.1.11 contextmenu: ^3.0.0 async: ^2.11.0 - sentry_flutter: ^7.13.1 + sentry_flutter: ^7.13.2 flutter_dotenv: ^5.1.0 flutter_reorderable_list: ^1.3.1 pie_chart: ^5.4.0 @@ -88,7 +88,7 @@ dev_dependencies: # rules and activating additional ones. flutter_lints: ^3.0.1 flutter_launcher_icons: ^0.13.1 - flutter_native_splash: ^2.2.10+1 + flutter_native_splash: ^2.3.6 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec