Files
Finlytic/FinlyticApp/lib/features/search/widgets/asset_search_dialog.dart
T

190 lines
7.4 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/network/api_client.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/widgets/asset_logo_widget.dart';
import '../../../core/widgets/shimmer_loading.dart';
import '../../../shared/widgets/favorite_star_button.dart';
import '../../asset_detail/views/asset_detail_screen.dart';
import '../bloc/search_bloc.dart';
import '../repositories/search_repository.dart';
/// Focused Spotlight Asset Search Dialog with reactive Favorite Star Button, Hero transitions, and Shimmer loading states.
class AssetSearchDialog extends StatelessWidget {
final ApiClient apiClient;
const AssetSearchDialog({
super.key,
required this.apiClient,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => SearchBloc(
repository: SearchRepository(apiClient: apiClient),
)..add(const SearchQueryChanged('')),
child: _AssetSearchDialogContent(apiClient: apiClient),
);
}
}
class _AssetSearchDialogContent extends StatefulWidget {
final ApiClient apiClient;
const _AssetSearchDialogContent({required this.apiClient});
@override
State<_AssetSearchDialogContent> createState() => _AssetSearchDialogContentState();
}
class _AssetSearchDialogContentState extends State<_AssetSearchDialogContent> {
final _searchController = TextEditingController();
@override
Widget build(BuildContext context) {
final activeTheme = AppTheme.activePreset;
return Dialog(
insetPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 40),
backgroundColor: activeTheme.cardSurface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(activeTheme.borderRadius + 2),
side: BorderSide(color: activeTheme.glassBorder, width: 1),
),
child: Container(
width: 600,
height: 520,
padding: const EdgeInsets.all(20),
child: Column(
children: [
BlocBuilder<SearchBloc, SearchState>(
builder: (context, state) {
final isLoading = state is SearchLoading;
return TextField(
controller: _searchController,
autofocus: true,
decoration: InputDecoration(
hintText: 'Aktienname oder ISIN suchen...',
prefixIcon: Icon(Icons.search, color: activeTheme.primaryColor),
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isLoading)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2, color: activeTheme.primaryColor),
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
),
onChanged: (query) {
context.read<SearchBloc>().add(SearchQueryChanged(query));
},
);
},
),
const SizedBox(height: 16),
Expanded(
child: BlocBuilder<SearchBloc, SearchState>(
builder: (context, state) {
if (state is SearchLoading) {
return ListView.builder(
itemCount: 5,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
ShimmerLoading(width: 36, height: 36, borderRadius: 10),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ShimmerLoading(width: 140, height: 14),
const SizedBox(height: 6),
ShimmerLoading(width: 80, height: 10),
],
),
),
],
),
),
);
}
if (state is SearchError) {
return Center(
child: Text(state.message, style: TextStyle(color: activeTheme.textMuted, fontSize: 13)),
);
}
if (state is SearchLoaded) {
final results = state.results;
if (results.isEmpty) {
return Center(
child: Text('Keine Wertpapiere gefunden', style: TextStyle(color: activeTheme.textMuted, fontSize: 13)),
);
}
return ListView.builder(
itemCount: results.length,
itemBuilder: (context, index) {
final item = results[index];
final assetName = item.displayName;
final isin = item.isin;
final targetId = isin.isNotEmpty ? isin : assetName;
return ListTile(
leading: AssetLogoWidget(
symbolOrName: targetId,
imageUrl: item.image,
size: 36,
),
title: Text(assetName, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
subtitle: isin.isNotEmpty
? Text('ISIN: $isin', style: TextStyle(color: activeTheme.textMuted, fontSize: 11))
: null,
trailing: SizedBox(
width: 40,
height: 40,
child: FavoriteStarButton(
identifier: targetId,
symbol: assetName,
name: assetName,
),
),
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => AssetDetailScreen(
isin: isin,
name: assetName,
apiClient: widget.apiClient,
),
),
);
},
);
},
);
}
return const SizedBox.shrink();
},
),
),
],
),
),
);
}
}