6 лаба сделана (поиск вроде тоже работает)

This commit is contained in:
mayday 2024-12-10 13:04:11 +04:00
parent 69f28b9297
commit 4014a00a68
9 changed files with 280 additions and 98 deletions

49
lib/bloc.dart Normal file
View File

@ -0,0 +1,49 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'events.dart';
import 'state.dart';
import 'character_service.dart';
import 'character.dart';
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
class DebouncedSearchCubit extends Cubit<String> {
DebouncedSearchCubit() : super('');
Timer? _debounce;
// Метод для обновления поиска с задержкой
void search(String query) {
if (_debounce?.isActive ?? false) _debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () {
emit(query);
});
}
@override
Future<void> close() {
_debounce?.cancel();
return super.close();
}
}
class HomeBloc extends Bloc<HomeEvent, HomeState> {
final CharacterService characterService;
HomeBloc(this.characterService) : super(const HomeState()) {
on<HomeLoadDataEvent>(_onLoadData);
}
Future<void> _onLoadData(HomeLoadDataEvent event, Emitter<HomeState> emit) async {
emit(state.copyWith(status: HomeStatus.loading));
try {
final characters = await characterService.getCharacters(search: event.searchQuery);
print('Characters loaded: $characters'); // Отладочный вывод
emit(state.copyWith(status: HomeStatus.loaded, characters: characters));
} catch (e) {
print('Error: $e');
emit(state.copyWith(status: HomeStatus.error, errorMessage: e.toString()));
}
}
}

View File

@ -59,7 +59,6 @@ class Character {
}
}
class Survivor extends Character {
Survivor({
required String name,

View File

@ -7,16 +7,31 @@ const String baseUrl = 'http://192.168.1.83:5000'; // IP-адрес вмест
class CharacterService {
Future<List<Character>> getCharacters({String search = ''}) async {
try {
final response = await http.get(Uri.parse('$baseUrl/characters?search=$search'));
// Формируем URL с параметром поиска
final uri = Uri.parse('$baseUrl/characters?search=$search');
// Выполняем HTTP-запрос
final response = await http.get(uri);
// Логирование данных для отладки
print('Response status: ${response.statusCode}');
print('Response body: ${response.body}'); // Выводим тело ответа для анализа
// Проверяем успешный ответ от сервера
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
print('Characters received: $data'); // Печать данных
// Возвращаем список объектов Character
return data.map((item) => Character.fromJson(item)).toList();
} else {
throw Exception('Ошибка загрузки данных с сервера. Статус: ${response.statusCode}');
// Ошибка, если сервер вернул не 200 статус
print('Error: Server responded with status ${response.statusCode}');
throw Exception('Ошибка загрузки данных с сервера');
}
} catch (e) {
print('Ошибка при получении данных: $e');
// Обработка ошибок при выполнении запроса
print('Error fetching characters: $e');
throw Exception('Не удалось загрузить персонажей');
}
}

17
lib/events.dart Normal file
View File

@ -0,0 +1,17 @@
import 'package:equatable/equatable.dart';
abstract class HomeEvent extends Equatable {
const HomeEvent();
@override
List<Object?> get props => [];
}
class HomeLoadDataEvent extends HomeEvent {
final String searchQuery;
const HomeLoadDataEvent({this.searchQuery = ''});
@override
List<Object?> get props => [searchQuery];
}

View File

@ -1,74 +1,72 @@
import 'package:flutter/material.dart';
import 'character.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'bloc.dart';
import 'events.dart';
import 'state.dart';
import 'character_service.dart';
import 'pages/CharacterDetailPage.dart';
import 'character.dart';
import 'pages/character_detail_page.dart';
void main() {
runApp(
MultiBlocProvider(
providers: [
BlocProvider<HomeBloc>(
create: (_) => HomeBloc(CharacterService()),
),
BlocProvider<DebouncedSearchCubit>(
create: (_) => DebouncedSearchCubit(),
),
],
child: MyApp(),
),
);
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Identity V Characters',
home: MyHomePage(title: 'Персонажи Identity V'),
title: 'My App',
home: MyHomePage(title: 'Identity'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
class MyHomePage extends StatelessWidget {
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final CharacterService _characterService = CharacterService();
late Future<List<Character>> _futureCharacters;
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_futureCharacters = _characterService.getCharacters();
}
void _searchCharacters(String query) {
setState(() {
_futureCharacters = _characterService.getCharacters(search: query);
});
}
MyHomePage({required this.title});
@override
Widget build(BuildContext context) {
// Загружаем данные сразу после инициализации страницы
context.read<HomeBloc>().add(HomeLoadDataEvent());
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
title: Text(title),
actions: [
IconButton(
icon: Icon(Icons.search),
onPressed: () {
showSearch(
context: context,
delegate: CharacterSearchDelegate(_searchCharacters),
delegate: CharacterSearchDelegate(),
);
},
),
],
),
body: FutureBuilder<List<Character>>(
future: _futureCharacters,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
body: BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
if (state.status == HomeStatus.loading) {
return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Ошибка: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text('Нет персонажей'));
} else {
final characters = snapshot.data!;
} else if (state.status == HomeStatus.error) {
return Center(child: Text('Ошибка: ${state.errorMessage}'));
} else if (state.status == HomeStatus.loaded) {
final characters = state.characters;
return ListView.builder(
itemCount: characters.length,
@ -81,27 +79,46 @@ class _MyHomePageState extends State<MyHomePage> {
),
title: Text(character.name),
subtitle: Text(character.typeString),
trailing: IconButton(
icon: Icon(
character.isLiked ? Icons.favorite : Icons.favorite_border,
color: character.isLiked ? Colors.red : null,
),
onPressed: () {
setState(() {
character.isLiked = !character.isLiked;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
character.isLiked
? '${character.name} понравился вам!'
: '${character.name} убран из лайков.',
),
duration: Duration(seconds: 2),
onTap: () {
// Переход на страницу с деталями персонажа
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CharacterDetailPage(character: character),
),
);
},
);
},
);
} else {
return Center(child: Text('Нет данных.'));
}
},
),
);
}
}
class CharacterSearchDelegate extends SearchDelegate {
@override
Widget buildSuggestions(BuildContext context) {
// Показываем предложения на основе текущего запроса
return BlocProvider<HomeBloc>(
create: (context) => HomeBloc(CharacterService()),
child: BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
final suggestions = state.characters
.where((character) => character.name.toLowerCase().contains(query.toLowerCase()))
.toList();
return ListView.builder(
itemCount: suggestions.length,
itemBuilder: (context, index) {
final character = suggestions[index];
return ListTile(
title: Text(character.name),
subtitle: Text(character.typeString),
onTap: () {
Navigator.push(
context,
@ -113,37 +130,35 @@ class _MyHomePageState extends State<MyHomePage> {
);
},
);
}
},
),
);
}
}
class CharacterSearchDelegate extends SearchDelegate {
final Function(String) onSearch;
CharacterSearchDelegate(this.onSearch);
@override
Widget buildSuggestions(BuildContext context) {
return ListView();
}
@override
Widget buildResults(BuildContext context) {
onSearch(query); // Выполняем поиск
return FutureBuilder<List<Character>>(
future: CharacterService().getCharacters(search: query), // Запрашиваем персонажей с фильтром
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// Отправляем запрос с задержкой
final debouncedSearchCubit = context.read<DebouncedSearchCubit>();
debouncedSearchCubit.search(query);
return BlocBuilder<DebouncedSearchCubit, String>(
builder: (context, searchQuery) {
// Отправляем запрос на сервер через HomeBloc
if (searchQuery.isNotEmpty) {
context.read<HomeBloc>().add(HomeLoadDataEvent(searchQuery: searchQuery));
}
return BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
if (state.status == HomeStatus.loading) {
return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Ошибка: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text('Нет результатов для "$query"'));
} else {
final characters = snapshot.data!;
} else if (state.status == HomeStatus.error) {
return Center(child: Text('Ошибка: ${state.errorMessage}'));
} else if (state.status == HomeStatus.loaded) {
final characters = state.characters
.where((character) => character.name.toLowerCase().contains(searchQuery.toLowerCase()))
.toList();
return ListView.builder(
itemCount: characters.length,
itemBuilder: (context, index) {
@ -151,12 +166,24 @@ class CharacterSearchDelegate extends SearchDelegate {
return ListTile(
title: Text(character.name),
subtitle: Text(character.typeString),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CharacterDetailPage(character: character),
),
);
},
);
},
);
} else {
return Center(child: Text('Нет результатов'));
}
},
);
},
);
}
@override
@ -165,7 +192,8 @@ class CharacterSearchDelegate extends SearchDelegate {
IconButton(
icon: Icon(Icons.clear),
onPressed: () {
query = ''; // Очищаем запрос
query = ''; // Очистить запрос
showSuggestions(context); // Показать предложения
},
),
];
@ -176,7 +204,7 @@ class CharacterSearchDelegate extends SearchDelegate {
return IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
close(context, null); // Закрываем поиск
close(context, null); // Закрыть поиск
},
);
}

View File

@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import '../character.dart';
import '../character.dart'; // Убедитесь, что путь правильный
class CharacterDetailPage extends StatelessWidget {
final Character character;
CharacterDetailPage({required this.character});
// Конструктор с обязательным параметром
const CharacterDetailPage({Key? key, required this.character}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -28,7 +29,6 @@ class CharacterDetailPage extends StatelessWidget {
),
SizedBox(height: 20),
Text(
character.name, // Имя персонажа
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),

33
lib/state.dart Normal file
View File

@ -0,0 +1,33 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import '../character.dart'; // Добавим импорт для Character
enum HomeStatus { initial, loading, loaded, error }
class HomeState extends Equatable {
final HomeStatus status;
final List<Character> characters; // Список персонажей
final String errorMessage;
const HomeState({
this.status = HomeStatus.initial,
this.characters = const [],
this.errorMessage = '',
});
// Метод для обновления состояния
HomeState copyWith({
HomeStatus? status,
List<Character>? characters,
String? errorMessage,
}) {
return HomeState(
status: status ?? this.status,
characters: characters ?? this.characters,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [status, characters, errorMessage];
}

View File

@ -9,6 +9,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
bloc:
dependency: transitive
description:
name: bloc
sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
url: "https://pub.dev"
source: hosted
version: "8.1.4"
boolean_selector:
dependency: transitive
description:
@ -49,6 +57,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.8"
equatable:
dependency: "direct main"
description:
name: equatable
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
url: "https://pub.dev"
source: hosted
version: "2.0.7"
fake_async:
dependency: transitive
description:
@ -62,6 +78,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_bloc:
dependency: "direct main"
description:
name: flutter_bloc
sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a
url: "https://pub.dev"
source: hosted
version: "8.1.6"
flutter_lints:
dependency: "direct dev"
description:
@ -147,6 +171,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.15.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
path:
dependency: transitive
description:
@ -155,6 +187,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.0"
provider:
dependency: transitive
description:
name: provider
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
url: "https://pub.dev"
source: hosted
version: "6.1.2"
sky_engine:
dependency: transitive
description: flutter

View File

@ -33,7 +33,8 @@ dependencies:
flutter:
sdk: flutter
http: ^1.2.2
equatable: ^2.0.5
flutter_bloc: ^8.1.5
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8