flutter analyze exits non-zero on info-level findings too, so all five had to go for `make pre-push` to pass. Four were mechanical. The fifth was not. envApiUser was reported as an unused declaration. Removing it revealed that the field behind it, _envApiUser, was then unused as well -- and the pair turns out to be a closed loop nothing could enter: the getter is public but sits on _DashboardContentState, a private class, so no caller outside this file could ever have reached it. The field was written once per session and never read. The debugPrint next to it logs envData.user directly, so the logging the comment describes never depended on the stored copy. Field, getter and assignment removed; _hasLoggedEnvUser stays, because it genuinely guards the log-once. Deleting the first warning exposing the second is the useful part: unused_field could not fire while a dead getter was "using" it. Dead code hides dead code. The two `if (x != null) x` collection entries become null-aware elements, which is the same intent spelled the way the SDK now expects. The two casts in data_grid_test were the second cast of a pair -- `mode as InfiniteDataMode` on the preceding line already promotes the local. flutter analyze: No issues found. The edited test file still passes all 37. Note the gate still prints "not gated here yet: test (T-56)" -- analysis is green, tests remain unwired, and that is deliberately left visible. Co-Authored-By: Claude <noreply@anthropic.com>
273 lines
6.5 KiB
Dart
273 lines
6.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// Reusable scaffold for entity pages (create, view, edit).
|
|
///
|
|
/// Provides consistent layout across all Control Room entity pages:
|
|
/// - AppBar with back button, title, and customizable actions
|
|
/// - Loading, error, and content states
|
|
/// - Consistent padding and styling
|
|
///
|
|
/// Usage:
|
|
/// ```dart
|
|
/// EntityPageScaffold(
|
|
/// title: 'Proxy Host Details',
|
|
/// onBack: () => Navigator.pop(context),
|
|
/// actions: [
|
|
/// IconButton(icon: Icon(Icons.edit), onPressed: onEdit),
|
|
/// ],
|
|
/// child: MyContent(),
|
|
/// )
|
|
/// ```
|
|
class EntityPageScaffold extends StatelessWidget {
|
|
const EntityPageScaffold({
|
|
super.key,
|
|
required this.title,
|
|
required this.child,
|
|
this.onBack,
|
|
this.actions,
|
|
this.floatingActionButton,
|
|
});
|
|
|
|
final String title;
|
|
final Widget child;
|
|
final VoidCallback? onBack;
|
|
final List<Widget>? actions;
|
|
final Widget? floatingActionButton;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
leading: onBack != null
|
|
? IconButton(
|
|
icon: const Icon(Icons.arrow_back),
|
|
onPressed: onBack,
|
|
)
|
|
: null,
|
|
title: Text(title),
|
|
actions: actions,
|
|
),
|
|
body: child,
|
|
floatingActionButton: floatingActionButton,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Async content wrapper with loading, error, and data states.
|
|
///
|
|
/// Use with Riverpod AsyncValue for consistent loading/error handling.
|
|
class EntityAsyncContent<T> extends StatelessWidget {
|
|
const EntityAsyncContent({
|
|
super.key,
|
|
required this.isLoading,
|
|
required this.error,
|
|
required this.data,
|
|
required this.onRetry,
|
|
required this.builder,
|
|
});
|
|
|
|
final bool isLoading;
|
|
final Object? error;
|
|
final T? data;
|
|
final VoidCallback onRetry;
|
|
final Widget Function(T data) builder;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
|
|
if (isLoading) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
|
|
if (error != null) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.error_outline, size: 48, color: colorScheme.error),
|
|
const SizedBox(height: 16),
|
|
const Text('Failed to load'),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
error.toString(),
|
|
style: TextStyle(color: colorScheme.outline),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 16),
|
|
FilledButton.icon(
|
|
onPressed: onRetry,
|
|
icon: const Icon(Icons.refresh),
|
|
label: const Text('Retry'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
if (data != null) {
|
|
return builder(data as T);
|
|
}
|
|
|
|
return const SizedBox.shrink();
|
|
}
|
|
}
|
|
|
|
/// Section header for entity detail pages.
|
|
///
|
|
/// Consistent styling for grouping related fields.
|
|
class EntitySection extends StatelessWidget {
|
|
const EntitySection({
|
|
super.key,
|
|
required this.title,
|
|
required this.icon,
|
|
required this.child,
|
|
this.trailing,
|
|
});
|
|
|
|
final String title;
|
|
final IconData icon;
|
|
final Widget child;
|
|
final Widget? trailing;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(icon, size: 20, color: colorScheme.primary),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
title,
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
color: colorScheme.primary,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
?trailing,
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
child,
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Row displaying a boolean setting with label and indicator.
|
|
class EntitySettingRow extends StatelessWidget {
|
|
const EntitySettingRow({
|
|
super.key,
|
|
required this.label,
|
|
required this.value,
|
|
this.subtitle,
|
|
});
|
|
|
|
final String label;
|
|
final bool value;
|
|
final String? subtitle;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(label),
|
|
if (subtitle != null)
|
|
Text(
|
|
subtitle!,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Theme.of(context).colorScheme.outline,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Icon(
|
|
value ? Icons.check_circle : Icons.cancel,
|
|
color: value ? Colors.green : Colors.grey,
|
|
size: 20,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Row displaying a key-value pair.
|
|
class EntityInfoRow extends StatelessWidget {
|
|
const EntityInfoRow({
|
|
super.key,
|
|
required this.label,
|
|
required this.value,
|
|
this.monospace = false,
|
|
});
|
|
|
|
final String label;
|
|
final String value;
|
|
final bool monospace;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SizedBox(
|
|
width: 120,
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(color: colorScheme.outline),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
value,
|
|
style: monospace
|
|
? const TextStyle(fontFamily: 'monospace')
|
|
: null,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Enum for entity page modes.
|
|
enum EntityPageMode {
|
|
create,
|
|
view,
|
|
edit,
|
|
}
|
|
|
|
/// Mixin for pages that support view/edit mode toggle.
|
|
///
|
|
/// Provides standard mode management for entity detail pages.
|
|
mixin EntityPageModeMixin<T extends StatefulWidget> on State<T> {
|
|
EntityPageMode _mode = EntityPageMode.view;
|
|
|
|
EntityPageMode get mode => _mode;
|
|
set mode(EntityPageMode value) => _mode = value;
|
|
|
|
bool get isViewing => _mode == EntityPageMode.view;
|
|
bool get isEditing => _mode == EntityPageMode.edit;
|
|
bool get isCreating => _mode == EntityPageMode.create;
|
|
|
|
void setMode(EntityPageMode newMode) {
|
|
setState(() => _mode = newMode);
|
|
}
|
|
|
|
void startEditing() => setMode(EntityPageMode.edit);
|
|
void stopEditing() => setMode(EntityPageMode.view);
|
|
}
|