feat(App): update Finlytic Flutter app UI and blocs
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
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/utils/asset_utils.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 isinCode = item.isinCode;
|
||||
|
||||
if (isinCode.isNotEmpty && assetName.isNotEmpty) {
|
||||
AssetUtils.registerAsset(isinCode, assetName, item.image);
|
||||
}
|
||||
|
||||
final targetId = isinCode.isNotEmpty ? isinCode : assetName;
|
||||
|
||||
return ListTile(
|
||||
leading: AssetLogoWidget(
|
||||
symbolOrName: targetId,
|
||||
imageUrl: item.image,
|
||||
size: 36,
|
||||
),
|
||||
title: Text(assetName, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
|
||||
subtitle: isinCode.isNotEmpty
|
||||
? Text('ISIN: $isinCode', 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(
|
||||
symbol: assetName,
|
||||
apiClient: widget.apiClient,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user