Improved MacOS support and organized screens and utils

This commit is contained in:
nullpeer
2025-11-30 12:49:33 +03:00
parent ae6fd57040
commit d793498d0a
56 changed files with 255 additions and 63 deletions

View File

@@ -0,0 +1,263 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/models/contact.dart';
class UserIdLookupScreen extends StatefulWidget {
const UserIdLookupScreen({super.key});
@override
State<UserIdLookupScreen> createState() => _UserIdLookupScreenState();
}
class _UserIdLookupScreenState extends State<UserIdLookupScreen> {
final TextEditingController _idController = TextEditingController();
final FocusNode _idFocusNode = FocusNode();
bool _isLoading = false;
Contact? _foundContact;
bool _searchAttempted = false;
Future<void> _searchById() async {
final String idText = _idController.text.trim();
if (idText.isEmpty) {
return;
}
final int? contactId = int.tryParse(idText);
if (contactId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Пожалуйста, введите корректный ID (только цифры)'),
),
);
return;
}
_idFocusNode.unfocus();
setState(() {
_isLoading = true;
_searchAttempted = true;
_foundContact = null;
});
try {
final List<Contact> contacts = await ApiService.instance
.fetchContactsByIds([contactId]);
if (mounted) {
setState(() {
_foundContact = contacts.isNotEmpty ? contacts.first : null;
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Ошибка при поиске: $e')));
}
}
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_idFocusNode.requestFocus();
});
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(title: const Text('Поиск по ID')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
TextField(
controller: _idController,
focusNode: _idFocusNode,
decoration: InputDecoration(
labelText: 'Введите ID пользователя',
filled: true,
fillColor: colors.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
prefixIcon: const Icon(Icons.person_search_outlined),
suffixIcon: _isLoading
? const Padding(
padding: EdgeInsets.all(12.0),
child: CircularProgressIndicator(strokeWidth: 2),
)
: IconButton(
icon: const Icon(Icons.search),
onPressed: _searchById,
),
),
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onSubmitted: (_) => _searchById(),
),
const SizedBox(height: 32),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _isLoading
? const Center(
key: ValueKey('loading'),
child: CircularProgressIndicator(),
)
: _searchAttempted
? _foundContact != null
? _buildContactCard(_foundContact!, colors)
: _buildEmptyState(
key: const ValueKey('not_found'),
colors: colors,
icon: Icons.search_off_rounded,
title: 'Пользователь не найден',
subtitle:
'Аккаунт с ID "${_idController.text}" не существует или скрыт.',
)
: _buildEmptyState(
key: const ValueKey('initial'),
colors: colors,
icon: Icons.person_search_rounded,
title: 'Введите ID для поиска',
subtitle: 'Найдем пользователя в системе по его ID',
),
),
],
),
),
);
}
Widget _buildContactCard(Contact contact, ColorScheme colors) {
return Column(
key: const ValueKey('contact_card'),
children: [
CircleAvatar(
radius: 56,
backgroundColor: colors.primaryContainer,
backgroundImage: contact.photoBaseUrl != null
? NetworkImage(contact.photoBaseUrl!)
: null,
child: contact.photoBaseUrl == null
? Text(
contact.name.isNotEmpty ? contact.name[0].toUpperCase() : '?',
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
color: colors.onPrimaryContainer,
),
)
: null,
),
const SizedBox(height: 16),
Text(
contact.name,
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
'ID: ${contact.id}',
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(color: colors.onSurfaceVariant),
),
const SizedBox(height: 24),
Container(
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
_buildInfoTile(
colors: colors,
icon: Icons.person_outlined,
title: 'Имя',
subtitle: contact.firstName,
),
const Divider(height: 1, indent: 16, endIndent: 16),
_buildInfoTile(
colors: colors,
icon: Icons.badge_outlined,
title: 'Фамилия',
subtitle: contact.lastName,
),
const Divider(height: 1, indent: 16, endIndent: 16),
_buildInfoTile(
colors: colors,
icon: Icons.notes_rounded,
title: 'Описание',
subtitle: contact.description,
),
],
),
),
],
);
}
Widget _buildInfoTile({
required ColorScheme colors,
required IconData icon,
required String title,
String? subtitle,
}) {
final bool hasData = subtitle != null && subtitle.isNotEmpty;
return ListTile(
leading: Icon(icon, color: colors.primary),
title: Text(title),
subtitle: Text(
hasData ? subtitle : '(не указано)',
style: TextStyle(
color: hasData
? colors.onSurfaceVariant
: colors.onSurfaceVariant.withOpacity(0.7),
fontStyle: hasData ? FontStyle.normal : FontStyle.italic,
),
),
);
}
Widget _buildEmptyState({
required Key key,
required ColorScheme colors,
required IconData icon,
required String title,
required String subtitle,
}) {
return Column(
key: key,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 64, color: colors.onSurfaceVariant.withOpacity(0.5)),
const SizedBox(height: 16),
Text(
title,
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(color: colors.onSurfaceVariant),
),
const SizedBox(height: 8),
Text(
subtitle,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colors.onSurfaceVariant.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
],
);
}
}