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
8 changes: 6 additions & 2 deletions lib/page/advanced_settings/advanced_settings_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ class _AdvancedSettingsViewState extends ConsumerState<AdvancedSettingsView> {
crossAxisCount: 2,
mainAxisSpacing: AppSpacing.lg,
crossAxisSpacing: AppSpacing.lg,
childAspectRatio: (430 / 56),
mainAxisExtent: 56,
childAspectRatio: (430 / 60),
mainAxisExtent: 60,
),
physics: const ScrollPhysics(),
itemCount: advancedSettings.length,
Expand Down Expand Up @@ -108,6 +108,10 @@ class _AdvancedSettingsViewState extends ConsumerState<AdvancedSettingsView> {
title: item.title,
trailing: AppIcon.font(AppFontIcons.chevronRight),
onTap: disabled ? null : item.onTap,
padding: EdgeInsets.symmetric(
vertical: AppSpacing.md,
horizontal: AppSpacing.xl,
),
),
);
}
Expand Down
57 changes: 52 additions & 5 deletions lib/page/dashboard/views/components/widgets/atomic/speed_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:privacy_gui/constants/build_config.dart';
import 'package:privacy_gui/localization/localization_hook.dart';
import 'package:privacy_gui/page/components/shortcuts/dialogs.dart';
import 'package:privacy_gui/page/dashboard/models/display_mode.dart';
import 'package:privacy_gui/page/dashboard/providers/dashboard_home_provider.dart';
import 'package:privacy_gui/page/dashboard/views/components/core/display_mode_widget.dart';
import 'package:privacy_gui/page/dashboard/views/components/widgets/parts/external_speed_test_links.dart';
import 'package:privacy_gui/page/dashboard/views/components/widgets/parts/internal_speed_test_result.dart';
import 'package:privacy_gui/page/health_check/models/health_check_enum.dart';
import 'package:privacy_gui/page/health_check/models/health_check_server.dart';
import 'package:privacy_gui/page/health_check/models/speed_test_ui_model.dart';
import 'package:privacy_gui/page/health_check/providers/health_check_provider.dart';
import 'package:privacy_gui/page/health_check/providers/health_check_state.dart';
Expand All @@ -30,6 +32,54 @@ class CustomSpeedTest extends DisplayModeConsumerWidget {
DisplayMode.expanded => 300,
};

/// Runs the speed test with server selection dialog.
Future<void> _runSpeedTestWithServerSelection(
BuildContext context,
WidgetRef ref,
) async {
final servers = ref.read(healthCheckProvider).servers;

// Show server selection if servers are available
if (servers.isNotEmpty) {
final selected = await _showServerSelectionDialog(context, servers);
if (selected == null) return; // User cancelled

ref.read(healthCheckProvider.notifier).setSelectedServer(selected);
}

// Run the speed test
ref.read(healthCheckProvider.notifier).runHealthCheck(Module.speedtest);
}

/// Shows a dialog for server selection.
Future<HealthCheckServer?> _showServerSelectionDialog(
BuildContext context,
List<HealthCheckServer> servers,
) async {
return showSimpleAppDialog<HealthCheckServer>(
context,
title: loc(context).selectServer,
content: Builder(
builder: (dialogContext) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: servers.map((server) {
return AppListTile(
key: Key('server_${server.serverID}'),
title: AppText.bodyMedium(server.serverName.isNotEmpty
? server.serverName
: server.serverHostname),
onTap: () => Navigator.of(dialogContext, rootNavigator: true)
.pop(server),
);
}).toList(),
);
},
),
);
}

@override
Widget buildCompactView(BuildContext context, WidgetRef ref) {
// Compact: Minimal view with just download/upload stats + controls
Expand Down Expand Up @@ -84,11 +134,8 @@ class CustomSpeedTest extends DisplayModeConsumerWidget {
child: isRunning
? const CircularProgressIndicator(strokeWidth: 2)
: IconButton(
onPressed: () {
ref
.read(healthCheckProvider.notifier)
.runHealthCheck(Module.speedtest);
},
onPressed: () =>
_runSpeedTestWithServerSelection(context, ref),
icon: const Icon(Icons.play_arrow, size: 20),
padding: EdgeInsets.zero,
color: Theme.of(context).colorScheme.primary,
Expand Down
14 changes: 9 additions & 5 deletions lib/page/dashboard/views/dashboard_menu_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -353,13 +353,17 @@ class AppMenuCard extends StatelessWidget {
),
if (title != null)
Padding(
padding: EdgeInsets.only(top: AppSpacing.sm),
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.start,
spacing: AppSpacing.sm,
padding: EdgeInsets.only(top: AppSpacing.xs),
child: Row(
children: [
AppText.titleSmall(title ?? ''),
Flexible(
child: AppText.titleSmall(
title ?? '',
overflow: TextOverflow.ellipsis,
),
),
if (isBeta) ...[
AppGap.sm(),
AppBadge(
label: 'BETA',
color: Theme.of(context)
Expand Down
120 changes: 99 additions & 21 deletions lib/page/health_check/shared_widgets/speed_test_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:privacy_gui/localization/localization_hook.dart';
import 'package:privacy_gui/page/components/customs/animated_digital_text.dart';
import 'package:privacy_gui/page/components/shortcuts/dialogs.dart';
import 'package:privacy_gui/page/health_check/models/health_check_enum.dart';
import 'package:privacy_gui/page/health_check/models/health_check_server.dart';
import 'package:privacy_gui/page/health_check/models/speed_test_ui_model.dart';
import 'package:privacy_gui/page/health_check/providers/health_check_provider.dart';
import 'package:privacy_gui/page/health_check/providers/health_check_state.dart';
Expand Down Expand Up @@ -46,6 +48,11 @@ class SpeedTestWidget extends ConsumerWidget {
/// If true, shows the result summary (download/upload) below the meter when complete.
final bool showResultSummary;

/// If true, shows a server selection dialog when the user taps the Go button
/// without a server selected. If false, the button is disabled when no server
/// is selected.
final bool showServerSelectionDialog;

const SpeedTestWidget({
super.key,
this.showDetails = true,
Expand All @@ -55,8 +62,41 @@ class SpeedTestWidget extends ConsumerWidget {
this.showLatestOnIdle = true,
this.meterSize,
this.showResultSummary = true,
this.showServerSelectionDialog = true,
});

/// Runs the speed test with optional server selection.
///
/// If [showServerSelectionDialog] is true and servers are available,
/// shows a dialog to select a server before running the test.
Future<void> _runSpeedTestWithServerSelection(
BuildContext context,
WidgetRef ref,
) async {
final isSpeedCheckSupported = ref
.read(healthCheckProvider.select((s) => s.isSpeedTestModuleSupported));

if (!isSpeedCheckSupported) {
// If not supported, navigate to the external test page.
context.pushNamed(RouteNamed.speedTestExternal);
return;
}

// Show server selection if enabled and servers are available
if (showServerSelectionDialog) {
final servers = ref.read(healthCheckProvider).servers;
if (servers.isNotEmpty) {
final selected = await _showServerSelectionDialog(context, servers);
if (selected == null) return; // User cancelled

ref.read(healthCheckProvider.notifier).setSelectedServer(selected);
}
}

// Run the speed test
ref.read(healthCheckProvider.notifier).runHealthCheck(Module.speedtest);
}

@override
Widget build(BuildContext context, WidgetRef ref) {
final healthCheckState = ref.watch(healthCheckProvider);
Expand Down Expand Up @@ -265,19 +305,15 @@ class SpeedTestWidget extends ConsumerWidget {
if (state.status == HealthCheckStatus.complete) {
if (!showResultSummary) {
return IconButton(
onPressed: () => ref
.read(healthCheckProvider.notifier)
.runHealthCheck(Module.speedtest),
onPressed: () => _runSpeedTestWithServerSelection(context, ref),
icon: Icon(Icons.replay,
color: Theme.of(context).colorScheme.primary),
tooltip: loc(context).testAgain,
);
}
// Show single-line result with tap-to-retry
return InkWell(
onTap: () => ref
.read(healthCheckProvider.notifier)
.runHealthCheck(Module.speedtest),
onTap: () => _runSpeedTestWithServerSelection(context, ref),
borderRadius: BorderRadius.circular(AppRadius.md),
child: Padding(
padding: const EdgeInsets.symmetric(
Expand All @@ -288,12 +324,24 @@ class SpeedTestWidget extends ConsumerWidget {
Icon(Icons.arrow_downward,
size: 14, color: Theme.of(context).colorScheme.primary),
AppGap.xs(),
AppText.titleSmall(result.downloadSpeed),
Flexible(
child: AppText.titleSmall(
result.downloadSpeed,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
AppGap.md(),
Icon(Icons.arrow_upward,
size: 14, color: Theme.of(context).colorScheme.primary),
AppGap.xs(),
AppText.titleSmall(result.uploadSpeed),
Flexible(
child: AppText.titleSmall(
result.uploadSpeed,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
AppGap.md(),
Container(
width: 1,
Expand Down Expand Up @@ -501,27 +549,19 @@ class SpeedTestWidget extends ConsumerWidget {
// indicatorPathStrokeWidth: 8,
// markerRadius: 2,
centerBuilder: (context, value) {
// Button is always enabled - the method handles server selection logic
final buttonColor = Theme.of(context).colorScheme.primary;

return SizedBox(
width: meterSize == null ? 102 : meterSize! / 2.0,
height: meterSize == null ? 102 : meterSize! / 2.0,
child: Material(
shape: const CircleBorder(),
color: Theme.of(context).colorScheme.primary,
color: buttonColor,
child: InkWell(
key: const Key('goBtn'),
customBorder: const CircleBorder(),
onTap: () {
final isSpeedCheckSupported = ref.watch(healthCheckProvider
.select((s) => s.isSpeedTestModuleSupported));
if (isSpeedCheckSupported) {
ref
.read(healthCheckProvider.notifier)
.runHealthCheck(Module.speedtest);
} else {
// If not supported, navigate to the external test page.
context.pushNamed(RouteNamed.speedTestExternal);
}
},
onTap: () => _runSpeedTestWithServerSelection(context, ref),
child: Center(
child: AppText.bodyLarge(loc(context).go,
color: Theme.of(context).colorScheme.onPrimary),
Expand Down Expand Up @@ -625,4 +665,42 @@ class SpeedTestWidget extends ConsumerWidget {
],
);
}

/// Shows a dialog for server selection.
/// Returns the selected server, or null if canceled.
Future<HealthCheckServer?> _showServerSelectionDialog(
BuildContext context,
List<HealthCheckServer> servers,
) async {
Comment on lines +671 to +674

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Unused ref parameter 📘 Rule violation ✓ Correctness

_showServerSelectionDialog declares a WidgetRef ref parameter that is never used.
• This typically triggers an analyzer hint (unused_parameter) and can fail CI if analyzer warnings
  are treated as violations.
• Keeping unused parameters also reduces code clarity and invites confusion about intended behavior.
Agent Prompt
## Issue description
`_showServerSelectionDialog` has an unused `WidgetRef ref` parameter, which can trigger analyzer warnings.

## Issue Context
The repository’s compliance requires no new analyzer/lint violations in modified Dart code.

## Fix Focus Areas
- lib/page/health_check/shared_widgets/speed_test_widget.dart[68-99]
- lib/page/health_check/shared_widgets/speed_test_widget.dart[670-699]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

return showSimpleAppDialog<HealthCheckServer>(
context,
title: loc(context).selectServer,
content: Builder(
builder: (dialogContext) {
return ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(dialogContext).size.height * 0.5,
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: servers.map((server) {
return AppListTile(
key: Key('server_${server.serverID}'),
title: AppText.bodyMedium(server.serverName.isNotEmpty
? server.serverName
: server.serverHostname),
onTap: () =>
Navigator.of(dialogContext, rootNavigator: true)
.pop(server),
);
}).toList(),
),
),
);
},
),
);
}
}
Loading