Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/core/utils/ipv6_address.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ Ipv6Scope classifyIpv6Scope(String address) {
bool isGlobalUnicastIpv6(String address) =>
classifyIpv6Scope(address) == Ipv6Scope.global;

/// Whether [address] is an IPv6 link-local address (`fe80::/10`).
///
/// Link-local addresses are only valid on a single link and are never a
/// meaningful address to surface in the UI. The single source of truth for the
/// `fe80::/10` range lives in `ipv6_ranges.dart`; this mirrors the public shape
/// of [isGlobalUnicastIpv6] so callers can filter without re-implementing the
/// scope test.
bool isLinkLocalIpv6(String address) =>
classifyIpv6Scope(address) == Ipv6Scope.linkLocal;

/// Returns [addresses] reordered so the most routable address comes first:
/// global unicast, then ULA, then link-local, then anything else. The relative
/// order of addresses that share a scope is preserved (stable sort), so the
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_ar.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPV6",
"ipv6Address": "عنوان IPv6",
"ipv6ScopeLinkLocal": "الرابط المحلي",
"keepAlive": "استمرار النشاط",
"l2tpPassthrough": "عبور من خلال L2TP",
"labelInterface": "الواجهة",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_da.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPV6",
"ipv6Address": "IPv6-adresse",
"ipv6ScopeLinkLocal": "Link-lokal",
"keepAlive": "Bevar tilslutningen",
"l2tpPassthrough": "L2TP Passthrough",
"labelInterface": "Grænseflade",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "IPv6-Adresse",
"ipv6ScopeLinkLocal": "Link-lokal",
"keepAlive": "Verbindung aufrecht halten",
"l2tpPassthrough": "L2TP-Passthrough",
"labelInterface": "Schnittstelle",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_el.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPV4",
"ipv6": "IPv6",
"ipv6Address": "Διεύθυνση IPv6",
"ipv6ScopeLinkLocal": "Τοπικό ζεύξης",
"keepAlive": "Διατήρηση σε ενεργή κατάσταση",
"l2tpPassthrough": "Διέλευση L2TP",
"labelInterface": "Διασύνδεση",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@
"description": "Do not translate"
},
"ipv6Address": "IPv6 Address",
"ipv6ScopeLinkLocal": "Link-local",
"keepAlive": "Keep alive",
"l2tpPassthrough": "L2TP Passthrough",
"labelInterface": "Interface",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_es.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "Dirección IPv6",
"ipv6ScopeLinkLocal": "Enlace local",
"keepAlive": "Mantener activo",
"l2tpPassthrough": "Paso a través de L2TP",
"labelInterface": "Interfaz",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_es_ar.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "Dirección IPv6",
"ipv6ScopeLinkLocal": "Enlace local",
"keepAlive": "Mantener activo",
"l2tpPassthrough": "Paso a través de L2TP",
"labelInterface": "Interfaz",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_fi.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "IPv6-osoite",
"ipv6ScopeLinkLocal": "Linkkikohtainen",
"keepAlive": "Aina käytössä",
"l2tpPassthrough": "L2TP-läpiohjaus",
"labelInterface": "Käyttöliittymä",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "Adresse IPv6",
"ipv6ScopeLinkLocal": "Lien-local",
"keepAlive": "Maintenir la connexion",
"l2tpPassthrough": "Passthrough L2TP",
"labelInterface": "Interface",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_fr_ca.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "Adresse IPv6",
"ipv6ScopeLinkLocal": "Lien-local",
"keepAlive": "Maintenir la connexion",
"l2tpPassthrough": "Intercommunication L2TP",
"labelInterface": "Interface",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_id.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPV4",
"ipv6": "IPv6",
"ipv6Address": "Alamat IPv6",
"ipv6ScopeLinkLocal": "Link-local",
"keepAlive": "Aktifkan terus",
"l2tpPassthrough": "L2TP Passthrough",
"labelInterface": "Antarmuka",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_it.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "Indirizzo IPv6",
"ipv6ScopeLinkLocal": "Link-local",
"keepAlive": "Connessione sempre attiva",
"l2tpPassthrough": "Passthrough L2TP",
"labelInterface": "Interfaccia",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_ja.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "IPv6 アドレス",
"ipv6ScopeLinkLocal": "リンクローカル",
"keepAlive": "接続を維持",
"l2tpPassthrough": "L2TP パススルー",
"labelInterface": "インターフェイス",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_ko.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "IPv6 주소",
"ipv6ScopeLinkLocal": "링크-로컬",
"keepAlive": "상태 유지",
"l2tpPassthrough": "L2TP 패스스루",
"labelInterface": "인터페이스",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_nb.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPV4",
"ipv6": "IPV6",
"ipv6Address": "IPv6-adresse",
"ipv6ScopeLinkLocal": "Lenkelokal",
"keepAlive": "Oppretthold",
"l2tpPassthrough": "L2TP-passasje",
"labelInterface": "Grensesnitt",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_nl.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "IPv6-adres",
"ipv6ScopeLinkLocal": "Link-lokaal",
"keepAlive": "Continu verbinding houden",
"l2tpPassthrough": "L2TP-doorvoer",
"labelInterface": "Interface",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_pl.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "Adres IPv6",
"ipv6ScopeLinkLocal": "Link-local",
"keepAlive": "Utrzymywanie aktywności",
"l2tpPassthrough": "L2TP Passthrough",
"labelInterface": "Interfejs",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_pt.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "Endereço IPv6",
"ipv6ScopeLinkLocal": "Link-local",
"keepAlive": "Funcionamento",
"l2tpPassthrough": "Passagem L2TP",
"labelInterface": "Interface",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_pt_pt.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "Endereço IPv6",
"ipv6ScopeLinkLocal": "Link-local",
"keepAlive": "Manter ligado",
"l2tpPassthrough": "Passagem L2TP",
"labelInterface": "Interface",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_ru.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "IPv6-адрес",
"ipv6ScopeLinkLocal": "Локальный для канала",
"keepAlive": "Проверка активности",
"l2tpPassthrough": "L2TP-туннель",
"labelInterface": "Интерфейс",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_sv.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "IPv6-adress",
"ipv6ScopeLinkLocal": "Länklokal",
"keepAlive": "Behåll anslutning",
"l2tpPassthrough": "L2TP-vidarekoppling",
"labelInterface": "Gränssnitt",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_th.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "IPv6 แอดเดรส",
"ipv6ScopeLinkLocal": "ลิงก์-โลคัล",
"keepAlive": "คงการเชื่อมต่อ",
"l2tpPassthrough": "L2TP Passthrough",
"labelInterface": "อินเตอร์เฟซ",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_tr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "IPv6 Adresi",
"ipv6ScopeLinkLocal": "Bağlantı yerel",
"keepAlive": "Bağlı tut",
"l2tpPassthrough": "L2TP Geçiş İzni",
"labelInterface": "Arabirim",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_vi.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "Địa chỉ IPv6",
"ipv6ScopeLinkLocal": "Link-local",
"keepAlive": "Giữ hoạt động",
"l2tpPassthrough": "L2TP Truyền qua",
"labelInterface": "Giao diện",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_zh.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "IPv6地址",
"ipv6ScopeLinkLocal": "链路本地",
"keepAlive": "保持活跃",
"l2tpPassthrough": "L2TP通道",
"labelInterface": "接口",
Expand Down
1 change: 1 addition & 0 deletions lib/l10n/app_zh_TW.arb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"ipv4": "IPv4",
"ipv6": "IPv6",
"ipv6Address": "IPv6 位址",
"ipv6ScopeLinkLocal": "連結本地",
"keepAlive": "保持活躍",
"l2tpPassthrough": "L2TP 穿透",
"labelInterface": "介面",
Expand Down
7 changes: 6 additions & 1 deletion lib/page/_shared/components/detail_widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,16 @@ class DetailCopyableTile extends StatelessWidget {
final String label;
final String value;

/// Optional widget that replaces the default leading [Icon] (e.g. an
/// [Ipv6ScopeBadge] carrying its own tooltip/semantics).
final Widget? leading;

const DetailCopyableTile({
super.key,
required this.icon,
required this.label,
required this.value,
this.leading,
});

@override
Expand All @@ -196,7 +201,7 @@ class DetailCopyableTile extends StatelessWidget {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 16, color: colorScheme.onSurfaceVariant),
leading ?? Icon(icon, size: 16, color: colorScheme.onSurfaceVariant),
AppGap.sm(),
Expanded(
child: Column(
Expand Down
53 changes: 49 additions & 4 deletions lib/page/_shared/components/layout_blocks/list_blocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,46 @@ class InfoGridItem {
final bool fullWidth;
final bool copyable;

/// Optional widget rendered next to the label (e.g. an [Ipv6ScopeBadge]).
final Widget? labelTrailing;

const InfoGridItem({
required this.label,
required this.value,
this.fullWidth = false,
this.copyable = false,
this.labelTrailing,
});
}

/// Icon-only marker for an IPv6 link-local (`fe80::/10`) address.
///
/// Link-local addresses are only valid on a single link and are not routable.
/// Wherever an IPv6 address is surfaced (dashboard cards and the device/node
/// detail views), a link-local address is shown but tagged with this compact
/// icon (`public_off` — mirroring the routable `public` icon used for WAN
/// addresses) rather than hidden. The tooltip provides discoverability and
/// doubles as the semantic label for screen readers, since the icon alone
/// carries no text.
class Ipv6ScopeBadge extends StatelessWidget {
final double? size;

const Ipv6ScopeBadge({super.key, this.size});

@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Tooltip(
message: loc(context).ipv6ScopeLinkLocal,
child: Icon(
Icons.public_off,
size: size ?? BlockConstants.iconSm,
color: colorScheme.onSurfaceVariant,
),
);
}
}

class _InfoGridTile extends StatelessWidget {
final InfoGridItem item;

Expand All @@ -179,10 +211,23 @@ class _InfoGridTile extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppText.labelSmall(
item.label.toUpperCase(),
color: colorScheme.onSurfaceVariant,
),
if (item.labelTrailing != null)
Row(
mainAxisSize: MainAxisSize.min,
children: [
AppText.labelSmall(
item.label.toUpperCase(),
color: colorScheme.onSurfaceVariant,
),
AppGap.xs(),
item.labelTrailing!,
],
)
else
AppText.labelSmall(
item.label.toUpperCase(),
color: colorScheme.onSurfaceVariant,
),
AppGap.xs(),
item.copyable
? _CopyableText(text: item.value)
Expand Down
17 changes: 15 additions & 2 deletions lib/page/devices/views/usp_device_detail_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:privacy_gui/route/navigation_extensions.dart';
import 'package:privacy_gui/components/ui_kit_page_view.dart';
import 'package:privacy_gui/localization/localization_hook.dart';
import 'package:privacy_gui/page/_shared/utils/device_classifier.dart';
import 'package:privacy_gui/core/utils/ipv6_address.dart';
import 'package:privacy_gui/core/utils/oui_lookup.dart';
import 'package:privacy_gui/route/constants.dart';
import 'package:privacy_gui/page/_shared/models/client_device.dart'
Expand Down Expand Up @@ -720,14 +721,26 @@ class _UspDeviceDetailViewState extends ConsumerState<UspDeviceDetailView> {
// IPv6 Section
// ===========================================================================

Widget _buildIpv6Section(BuildContext context, List<String> addresses) {
Widget _buildIpv6Section(BuildContext context, List<String> rawAddresses) {
final colorScheme = Theme.of(context).colorScheme;
// Surface globally routable addresses first so the collapsed view (which
// shows only the first entry) never leads with a link-local address, while
// still enumerating every address when expanded. See #1128/#1129.
final addresses = preferGlobalIpv6First(rawAddresses);
final displayCount = _ipv6Expanded ? addresses.length : 1;
// When the representative (first) address is link-local, the leading icon
// itself signals the scope (public_off + tooltip) instead of a duplicate
// trailing badge. See #1128/#1129.
final leadingIsLinkLocal =
addresses.isNotEmpty && isLinkLocalIpv6(addresses.first);

return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.language, size: 16, color: colorScheme.onSurfaceVariant),
if (leadingIsLinkLocal)
const Ipv6ScopeBadge(size: 16)
else
Icon(Icons.language, size: 16, color: colorScheme.onSurfaceVariant),
AppGap.sm(),
Expanded(
child: Column(
Expand Down
7 changes: 7 additions & 0 deletions lib/page/internet_settings/cards/usp_network_status_card.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:privacy_gui/core/utils/ipv6_address.dart';
import 'package:privacy_gui/localization/localization_hook.dart';
import 'package:privacy_gui/page/_shared/models/wan_status_ui_model.dart';
import 'package:privacy_gui/page/_shared/components/dashboard_card_template.dart';
Expand Down Expand Up @@ -102,6 +103,12 @@ class UspNetworkStatusCard extends ConsumerWidget {
label: 'IPv6',
value: wan.ipv6Addresses.first,
copyable: true,
// The representative address prefers global unicast; when only
// a link-local (fe80::/10) address exists it is still shown,
// tagged with a scope badge rather than hidden. See #1128.
labelTrailing: isLinkLocalIpv6(wan.ipv6Addresses.first)
? const Ipv6ScopeBadge()
: null,
)
else if (wan.ipv6Enabled)
InfoGridItem(label: 'IPv6', value: 'Enabled'),
Expand Down
Loading
Loading