mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Align navigation with Material 3 responsive recommendations
- Replace single NavigationRail with adaptive navigation: NavigationBar (bottom) for compact <600dp, icon-only NavigationRail for medium 600–840dp, and extended NavigationRail for expanded ≥840dp - Remove nested Scaffold/AppBar from About, Settings, DeviceList, and DeviceDetail — all screens now render as plain widgets inside the single shell Scaffold; page titles surface as inline headings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
ebcde0ff4a
commit
e5c27f067c
@@ -15,55 +15,54 @@ class About extends StatelessWidget {
|
|||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final textTheme = Theme.of(context).textTheme;
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
|
||||||
return Scaffold(
|
return SingleChildScrollView(
|
||||||
appBar: AppBar(title: const Text('About OOTT')),
|
padding: const EdgeInsets.all(24),
|
||||||
body: SingleChildScrollView(
|
child: Column(
|
||||||
padding: const EdgeInsets.all(24),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Text('About OOTT', style: textTheme.headlineSmall),
|
||||||
children: [
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'Easy to setup and use network device discovery and alert system. '
|
'Easy to setup and use network device discovery and alert system. '
|
||||||
'Notifies you when new or unknown devices join your local area network.',
|
'Notifies you when new or unknown devices join your local area network.',
|
||||||
style: textTheme.bodyLarge,
|
style: textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'v$_version - released $_releaseDate',
|
||||||
|
style: textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
Text(
|
const SizedBox(height: 24),
|
||||||
'v$_version - released $_releaseDate',
|
_SurfaceContainer(
|
||||||
style: textTheme.bodyMedium?.copyWith(
|
colorScheme: colorScheme,
|
||||||
color: colorScheme.onSurfaceVariant,
|
child: Column(
|
||||||
),
|
children: [
|
||||||
|
_LinkRow(
|
||||||
|
icon: Icons.code,
|
||||||
|
label: 'Source code',
|
||||||
|
url: _repoUrl,
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
textTheme: textTheme,
|
||||||
|
),
|
||||||
|
Divider(
|
||||||
|
height: 1,
|
||||||
|
color: colorScheme.outlineVariant,
|
||||||
|
indent: 16,
|
||||||
|
endIndent: 16,
|
||||||
|
),
|
||||||
|
_LicenseRow(colorScheme: colorScheme, textTheme: textTheme),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
),
|
||||||
_SurfaceContainer(
|
const SizedBox(height: 16),
|
||||||
colorScheme: colorScheme,
|
_SurfaceContainer(
|
||||||
child: Column(
|
colorScheme: colorScheme,
|
||||||
children: [
|
padding: const EdgeInsets.all(16),
|
||||||
_LinkRow(
|
child: _NoticesSection(textTheme: textTheme),
|
||||||
icon: Icons.code,
|
),
|
||||||
label: 'Source code',
|
],
|
||||||
url: _repoUrl,
|
|
||||||
colorScheme: colorScheme,
|
|
||||||
textTheme: textTheme,
|
|
||||||
),
|
|
||||||
Divider(
|
|
||||||
height: 1,
|
|
||||||
color: colorScheme.outlineVariant,
|
|
||||||
indent: 16,
|
|
||||||
endIndent: 16,
|
|
||||||
),
|
|
||||||
_LicenseRow(colorScheme: colorScheme, textTheme: textTheme),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_SurfaceContainer(
|
|
||||||
colorScheme: colorScheme,
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: _NoticesSection(textTheme: textTheme),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,66 +53,80 @@ class _DeviceDetailState extends State<DeviceDetail> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final device = _device;
|
final device = _device;
|
||||||
|
|
||||||
return Scaffold(
|
return Column(
|
||||||
appBar: AppBar(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
title: const Text('Device Details'),
|
children: [
|
||||||
leading: BackButton(onPressed: () => context.pop()),
|
Row(
|
||||||
actions: [
|
children: [
|
||||||
if (device != null)
|
BackButton(onPressed: () => context.pop()),
|
||||||
PopupMenuButton<String>(
|
Expanded(
|
||||||
icon: const Icon(Icons.more_vert),
|
child: Text(
|
||||||
onSelected: (value) async {
|
'Device Details',
|
||||||
if (value == 'forget') {
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
await confirmForgetDevice(context, device, _loadDevice);
|
|
||||||
} else if (value == 'register') {
|
|
||||||
await showRegisterDeviceDialog(context, device, _loadDevice);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemBuilder: (context) => [
|
|
||||||
if (device.isRegistered)
|
|
||||||
const PopupMenuItem(value: 'forget', child: Text('Forget')),
|
|
||||||
if (!device.isRegistered)
|
|
||||||
const PopupMenuItem(
|
|
||||||
value: 'register',
|
|
||||||
child: Text('Register'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: _isLoading
|
|
||||||
? const Center(child: CircularProgressIndicator())
|
|
||||||
: _error != null
|
|
||||||
? Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text('Error: $_error'),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: _loadDevice,
|
|
||||||
child: const Text('Retry'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: device == null
|
|
||||||
? const Center(child: Text('Device not found'))
|
|
||||||
: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_DeviceHeader(device: device),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
_DeviceInfoCard(device: device),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
_SectionHeader(title: 'Event History'),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
DeviceEventHistory(device: device),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (device != null)
|
||||||
|
PopupMenuButton<String>(
|
||||||
|
icon: const Icon(Icons.more_vert),
|
||||||
|
onSelected: (value) async {
|
||||||
|
if (value == 'forget') {
|
||||||
|
await confirmForgetDevice(context, device, _loadDevice);
|
||||||
|
} else if (value == 'register') {
|
||||||
|
await showRegisterDeviceDialog(
|
||||||
|
context,
|
||||||
|
device,
|
||||||
|
_loadDevice,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
if (device.isRegistered)
|
||||||
|
const PopupMenuItem(value: 'forget', child: Text('Forget')),
|
||||||
|
if (!device.isRegistered)
|
||||||
|
const PopupMenuItem(
|
||||||
|
value: 'register',
|
||||||
|
child: Text('Register'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: _isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: _error != null
|
||||||
|
? Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('Error: $_error'),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: _loadDevice,
|
||||||
|
child: const Text('Retry'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: device == null
|
||||||
|
? const Center(child: Text('Device not found'))
|
||||||
|
: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_DeviceHeader(device: device),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_DeviceInfoCard(device: device),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_SectionHeader(title: 'Event History'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
DeviceEventHistory(device: device),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,64 +119,64 @@ class _DeviceListState extends State<DeviceList> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final formatter = FriendlyDateFormatter();
|
final formatter = FriendlyDateFormatter();
|
||||||
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
|
||||||
return Scaffold(
|
return Column(
|
||||||
appBar: AppBar(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
title: const Text('Devices'),
|
children: [
|
||||||
actions: [
|
Row(
|
||||||
Badge(
|
children: [
|
||||||
isLabelVisible: _hasActiveDetailFilters,
|
Expanded(child: Text('Devices', style: textTheme.headlineSmall)),
|
||||||
child: IconButton(
|
Badge(
|
||||||
icon: const Icon(Icons.filter_list),
|
isLabelVisible: _hasActiveDetailFilters,
|
||||||
tooltip: 'Filter',
|
child: IconButton(
|
||||||
onPressed: () => _showFilterSheet(context),
|
icon: const Icon(Icons.filter_list),
|
||||||
),
|
tooltip: 'Filter',
|
||||||
),
|
onPressed: () => _showFilterSheet(context),
|
||||||
],
|
|
||||||
bottom: PreferredSize(
|
|
||||||
preferredSize: const Size.fromHeight(48),
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
|
||||||
child: Wrap(
|
|
||||||
spacing: 8.0,
|
|
||||||
children: _DeviceFilter.values
|
|
||||||
.map(
|
|
||||||
(f) => ChoiceChip(
|
|
||||||
label: Text(f.label),
|
|
||||||
selected: _filter == f,
|
|
||||||
onSelected: (_) {
|
|
||||||
setState(() => _filter = f);
|
|
||||||
_loadDevices();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 8.0,
|
||||||
|
children: _DeviceFilter.values
|
||||||
|
.map(
|
||||||
|
(f) => ChoiceChip(
|
||||||
|
label: Text(f.label),
|
||||||
|
selected: _filter == f,
|
||||||
|
onSelected: (_) {
|
||||||
|
setState(() => _filter = f);
|
||||||
|
_loadDevices();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
body: _isLoading
|
child: _isLoading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: _error != null
|
: _error != null
|
||||||
? Center(child: Text('Error: $_error'))
|
? Center(child: Text('Error: $_error'))
|
||||||
: _devices.isEmpty
|
: _devices.isEmpty
|
||||||
? Center(child: Text(_emptyMessage()))
|
? Center(child: Text(_emptyMessage()))
|
||||||
: RefreshIndicator(
|
: RefreshIndicator(
|
||||||
onRefresh: _loadDevices,
|
|
||||||
child: ListView.builder(
|
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
|
||||||
itemCount: _devices.length,
|
|
||||||
itemBuilder: (context, index) => _DeviceCard(
|
|
||||||
device: _devices[index],
|
|
||||||
formatter: formatter,
|
|
||||||
onRefresh: _loadDevices,
|
onRefresh: _loadDevices,
|
||||||
|
child: ListView.builder(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
itemCount: _devices.length,
|
||||||
|
itemBuilder: (context, index) => _DeviceCard(
|
||||||
|
device: _devices[index],
|
||||||
|
formatter: formatter,
|
||||||
|
onRefresh: _loadDevices,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,34 @@ import 'home/home_screen.dart';
|
|||||||
import 'status/status_screen.dart';
|
import 'status/status_screen.dart';
|
||||||
import 'utils/pref_utils.dart';
|
import 'utils/pref_utils.dart';
|
||||||
|
|
||||||
|
// M3 window size class breakpoints
|
||||||
|
const _mediumBreakpoint = 600.0;
|
||||||
|
const _expandedBreakpoint = 840.0;
|
||||||
|
|
||||||
|
typedef _NavDest = ({IconData icon, IconData activeIcon, String label});
|
||||||
|
|
||||||
|
const List<_NavDest> _destinations = [
|
||||||
|
(icon: Icons.home_outlined, activeIcon: Icons.home, label: 'Home'),
|
||||||
|
(
|
||||||
|
icon: Icons.devices_other_outlined,
|
||||||
|
activeIcon: Icons.devices_other,
|
||||||
|
label: 'Devices',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
icon: Icons.monitor_heart_outlined,
|
||||||
|
activeIcon: Icons.monitor_heart,
|
||||||
|
label: 'Status',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
icon: Icons.settings_outlined,
|
||||||
|
activeIcon: Icons.settings,
|
||||||
|
label: 'Settings',
|
||||||
|
),
|
||||||
|
(icon: Icons.info_outline, activeIcon: Icons.info, label: 'About'),
|
||||||
|
];
|
||||||
|
|
||||||
// Routes definitions
|
// Routes definitions
|
||||||
final GoRouter router = GoRouter(
|
final GoRouter router = GoRouter(
|
||||||
// If there is no API base URL send the user to settings
|
|
||||||
initialLocation: '/notifications',
|
initialLocation: '/notifications',
|
||||||
routes: [
|
routes: [
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
@@ -72,27 +97,35 @@ class MainShell extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return Scaffold(
|
final selectedIndex = _calculateSelectedIndex(context);
|
||||||
appBar: AppBar(
|
final width = constraints.maxWidth;
|
||||||
title: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 2),
|
if (width < _mediumBreakpoint) {
|
||||||
decoration: BoxDecoration(
|
return Scaffold(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
appBar: _buildAppBar(context),
|
||||||
borderRadius: BorderRadius.circular(10),
|
body: Padding(
|
||||||
),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
child: Text(
|
child: child,
|
||||||
'OOTT',
|
|
||||||
style: GoogleFonts.barlowCondensed(
|
|
||||||
color: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 26,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
backgroundColor: Theme.of(
|
bottomNavigationBar: NavigationBar(
|
||||||
context,
|
selectedIndex: selectedIndex,
|
||||||
).colorScheme.surfaceContainerLowest,
|
onDestinationSelected: (index) =>
|
||||||
),
|
_onDestinationSelected(index, context),
|
||||||
|
destinations: _destinations
|
||||||
|
.map(
|
||||||
|
(d) => NavigationDestination(
|
||||||
|
icon: Icon(d.icon),
|
||||||
|
selectedIcon: Icon(d.activeIcon),
|
||||||
|
label: d.label,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: _buildAppBar(context),
|
||||||
body: Row(
|
body: Row(
|
||||||
children: [
|
children: [
|
||||||
SafeArea(
|
SafeArea(
|
||||||
@@ -100,35 +133,17 @@ class MainShell extends StatelessWidget {
|
|||||||
backgroundColor: Theme.of(
|
backgroundColor: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.surfaceContainerLow,
|
).colorScheme.surfaceContainerLow,
|
||||||
extended: constraints.maxWidth >= 600,
|
extended: width >= _expandedBreakpoint,
|
||||||
destinations: [
|
destinations: _destinations
|
||||||
NavigationRailDestination(
|
.map(
|
||||||
icon: Icon(Icons.home_outlined),
|
(d) => NavigationRailDestination(
|
||||||
selectedIcon: Icon(Icons.home),
|
icon: Icon(d.icon),
|
||||||
label: Text('Home'),
|
selectedIcon: Icon(d.activeIcon),
|
||||||
),
|
label: Text(d.label),
|
||||||
NavigationRailDestination(
|
),
|
||||||
icon: Icon(Icons.devices_other_outlined),
|
)
|
||||||
selectedIcon: Icon(Icons.devices_other),
|
.toList(),
|
||||||
label: Text('Devices'),
|
selectedIndex: selectedIndex,
|
||||||
),
|
|
||||||
NavigationRailDestination(
|
|
||||||
icon: Icon(Icons.monitor_heart_outlined),
|
|
||||||
selectedIcon: Icon(Icons.monitor_heart),
|
|
||||||
label: Text('Status'),
|
|
||||||
),
|
|
||||||
NavigationRailDestination(
|
|
||||||
icon: Icon(Icons.settings_outlined),
|
|
||||||
selectedIcon: Icon(Icons.settings),
|
|
||||||
label: Text('Settings'),
|
|
||||||
),
|
|
||||||
NavigationRailDestination(
|
|
||||||
icon: Icon(Icons.info_outline),
|
|
||||||
selectedIcon: Icon(Icons.info),
|
|
||||||
label: Text('About'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
selectedIndex: _calculateSelectedIndex(context),
|
|
||||||
onDestinationSelected: (index) =>
|
onDestinationSelected: (index) =>
|
||||||
_onDestinationSelected(index, context),
|
_onDestinationSelected(index, context),
|
||||||
),
|
),
|
||||||
@@ -146,6 +161,27 @@ class MainShell extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppBar _buildAppBar(BuildContext context) {
|
||||||
|
return AppBar(
|
||||||
|
title: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'OOTT',
|
||||||
|
style: GoogleFonts.barlowCondensed(
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 26,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLowest,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _redirectToSettings() {
|
String? _redirectToSettings() {
|
||||||
|
|||||||
@@ -79,13 +79,15 @@ class _SettingsState extends State<Settings> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final appColors = Theme.of(context).extension<AppColorExtension>()!;
|
final appColors = Theme.of(context).extension<AppColorExtension>()!;
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
|
||||||
return Scaffold(
|
return SingleChildScrollView(
|
||||||
appBar: AppBar(title: const Text('Settings')),
|
child: Form(
|
||||||
body: Form(
|
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
Text('Settings', style: textTheme.headlineSmall),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _baseUrlController,
|
controller: _baseUrlController,
|
||||||
|
|||||||
Reference in New Issue
Block a user