initial commit (migrated)

This commit is contained in:
fiatcode 2025-10-20 16:43:59 +07:00
commit b594facb51
143 changed files with 11057 additions and 0 deletions

View file

@ -0,0 +1,37 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kuwot/core/domain/no_params.dart';
import 'package:kuwot/core/presentation/bloc/error_state.dart';
import 'package:kuwot/features/quote/domain/entities/background_image.dart';
import 'package:kuwot/features/quote/domain/use_cases/get_background_images.dart';
part 'background_images_events.dart';
part 'background_images_states.dart';
class BackgroundImagesBloc
extends Bloc<BackgroundImagesEvent, BackgroundImagesState> {
final GetBackgroundImages getBackgroundImages;
BackgroundImagesBloc({required this.getBackgroundImages})
: super(const BackgroundImagesInitialState()) {
on<GetBackgroundImagesEvent>(_onGetBackgroundImages);
}
Future<void> _onGetBackgroundImages(
GetBackgroundImagesEvent event,
Emitter<BackgroundImagesState> emit,
) async {
emit(const BackgroundImagesLoadingState());
final result = await getBackgroundImages(const NoParams());
result.fold(
(failure) => emit(
BackgroundImagesErrorState(
message: failure.message,
cause: failure.cause,
),
),
(photos) => emit(BackgroundImagesLoadedState(photos)),
);
}
}

View file

@ -0,0 +1,12 @@
part of 'background_images_bloc.dart';
abstract class BackgroundImagesEvent extends Equatable {
const BackgroundImagesEvent();
}
class GetBackgroundImagesEvent extends BackgroundImagesEvent {
const GetBackgroundImagesEvent();
@override
List<Object> get props => [];
}

View file

@ -0,0 +1,45 @@
part of 'background_images_bloc.dart';
abstract class BackgroundImagesState extends Equatable {
const BackgroundImagesState();
@override
String toString() => runtimeType.toString();
}
class BackgroundImagesInitialState extends BackgroundImagesState {
const BackgroundImagesInitialState();
@override
List<Object> get props => [];
}
class BackgroundImagesLoadingState extends BackgroundImagesState {
const BackgroundImagesLoadingState();
@override
List<Object> get props => [];
}
class BackgroundImagesLoadedState extends BackgroundImagesState {
final List<BackgroundImage> backgroundImages;
const BackgroundImagesLoadedState(this.backgroundImages);
@override
List<Object> get props => [backgroundImages];
}
class BackgroundImagesErrorState extends BackgroundImagesState
implements ErrorState {
@override
final String message;
@override
final Exception? cause;
const BackgroundImagesErrorState({required this.message, this.cause});
@override
List<Object?> get props => [message, cause];
}

View file

@ -0,0 +1,83 @@
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kuwot/core/data/local/config.dart';
import 'package:kuwot/core/data/local/translation_target_config.dart';
import 'package:kuwot/core/presentation/bloc/error_state.dart';
import 'package:kuwot/features/quote/domain/entities/quote.dart';
import 'package:kuwot/features/quote/domain/use_cases/get_quote.dart';
import 'package:kuwot/features/quote/domain/use_cases/get_translated_quote.dart';
part 'quote_events.dart';
part 'quote_states.dart';
class QuoteBloc extends Bloc<QuoteEvent, QuoteState> {
final GetQuote getQuote;
final GetTranslatedQuote getTranslatedQuote;
final Config<TranslationTarget> translationTargetConfig;
int _originalQuoteId = -1;
QuoteBloc({
required this.getQuote,
required this.getTranslatedQuote,
required this.translationTargetConfig,
}) : super(const QuoteInitialState()) {
on<GetQuoteEvent>(_onGetQuoteEvent, transformer: droppable());
on<GetTranslatedQuoteEvent>(
_onGetTranslatedQuoteEvent,
transformer: droppable(),
);
}
Future<void> _onGetQuoteEvent(
QuoteEvent event,
Emitter<QuoteState> emit,
) async {
emit(const QuoteLoadingState());
// add delay to limit the number of api calls
await Future.delayed(const Duration(seconds: 1));
// check if need to translate the quote
final translationTarget = await translationTargetConfig.get();
// get quote
final result = await getQuote(GetQuoteParams(translationTarget));
result.fold((failure) => emit(QuoteErrorState(message: failure.message)), (
quote,
) {
_originalQuoteId = quote.id;
emit(QuoteLoadedState(quote: quote));
});
}
Future<void> _onGetTranslatedQuoteEvent(
GetTranslatedQuoteEvent event,
Emitter<QuoteState> emit,
) async {
emit(const QuoteLoadingState());
// add delay to limit the number of api calls
await Future.delayed(const Duration(seconds: 1));
// get translation target
final translationTarget = await translationTargetConfig.get();
if (translationTarget == null) {
emit(const QuoteErrorState(message: 'Translation target is not set'));
return;
}
// get translated quote
final result = await getTranslatedQuote(
GetTranslatedQuoteParams(
id: _originalQuoteId,
translationTarget: translationTarget,
),
);
result.fold(
(failure) => emit(QuoteErrorState(message: failure.message)),
(quote) => emit(QuoteLoadedState(quote: quote)),
);
}
}

View file

@ -0,0 +1,21 @@
part of 'quote_bloc.dart';
abstract class QuoteEvent extends Equatable {
const QuoteEvent();
}
class GetQuoteEvent extends QuoteEvent {
const GetQuoteEvent();
@override
List<Object> get props => [];
}
class GetTranslatedQuoteEvent extends QuoteEvent {
final TranslationTarget translationTarget;
const GetTranslatedQuoteEvent(this.translationTarget);
@override
List<Object> get props => [translationTarget];
}

View file

@ -0,0 +1,44 @@
part of 'quote_bloc.dart';
abstract class QuoteState extends Equatable {
const QuoteState();
@override
String toString() => runtimeType.toString();
}
class QuoteInitialState extends QuoteState {
const QuoteInitialState();
@override
List<Object> get props => [];
}
class QuoteLoadingState extends QuoteState {
const QuoteLoadingState();
@override
List<Object> get props => [];
}
class QuoteLoadedState extends QuoteState {
final Quote quote;
const QuoteLoadedState({required this.quote});
@override
List<Object> get props => [quote];
}
class QuoteErrorState extends QuoteState implements ErrorState {
@override
final String message;
@override
final Exception? cause;
const QuoteErrorState({required this.message, this.cause});
@override
List<Object?> get props => [message, cause];
}

View file

@ -0,0 +1,31 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kuwot/core/domain/no_params.dart';
import 'package:kuwot/core/presentation/bloc/error_state.dart';
import 'package:kuwot/features/quote/domain/entities/translation.dart';
import 'package:kuwot/features/quote/domain/use_cases/get_translations.dart';
part 'translations_events.dart';
part 'translations_states.dart';
class TranslationsBloc extends Bloc<TranslationsEvent, TranslationsState> {
final GetTranslations getTranslations;
TranslationsBloc({required this.getTranslations})
: super(const TranslationsInitialState()) {
on<GetTranslationsEvent>(_onGetTranslationEvent);
}
Future<void> _onGetTranslationEvent(
TranslationsEvent event,
Emitter<TranslationsState> emit,
) async {
emit(const TranslationsLoadingState());
final translations = await getTranslations(const NoParams());
translations.fold(
(failure) => emit(TranslationsErrorState(message: failure.message)),
(translations) =>
emit(TranslationsLoadedState(translations: translations)),
);
}
}

View file

@ -0,0 +1,12 @@
part of 'translations_bloc.dart';
abstract class TranslationsEvent extends Equatable {
const TranslationsEvent();
}
class GetTranslationsEvent extends TranslationsEvent {
const GetTranslationsEvent();
@override
List<Object> get props => [];
}

View file

@ -0,0 +1,41 @@
part of 'translations_bloc.dart';
abstract class TranslationsState extends Equatable {
const TranslationsState();
}
class TranslationsInitialState extends TranslationsState {
const TranslationsInitialState();
@override
List<Object> get props => [];
}
class TranslationsLoadingState extends TranslationsState {
const TranslationsLoadingState();
@override
List<Object> get props => [];
}
class TranslationsLoadedState extends TranslationsState {
final List<Translation> translations;
const TranslationsLoadedState({required this.translations});
@override
List<Object> get props => [translations];
}
class TranslationsErrorState extends TranslationsState implements ErrorState {
@override
final String message;
@override
final Exception? cause;
const TranslationsErrorState({required this.message, this.cause});
@override
List<Object?> get props => [message, cause];
}

View file

@ -0,0 +1,209 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:kuwot/core/data/local/translation_target_config.dart';
import 'package:kuwot/core/router/app_router.gr.dart';
import 'package:kuwot/features/quote/data/data_sources/remote/kuwot_api_remote_data_source.dart';
import 'package:kuwot/features/quote/presentation/bloc/background_images_bloc.dart';
import 'package:kuwot/features/quote/presentation/bloc/quote_bloc.dart';
import 'package:kuwot/features/quote/presentation/widgets/background_image_widget.dart';
import 'package:kuwot/features/quote/presentation/widgets/quote_widget.dart';
import 'package:kuwot/features/quote/presentation/widgets/translate_target_dialog.dart';
import 'package:screenshot/screenshot.dart';
import 'package:share_plus/share_plus.dart';
@RoutePage()
class QuotePage extends StatefulWidget {
const QuotePage({super.key});
@override
State<QuotePage> createState() => _QuotePageState();
}
class _QuotePageState extends State<QuotePage> {
final _screenshotController = ScreenshotController();
int _backgroundIndex = 0;
bool _isSharingQuote = false;
late QuoteBloc _dailyQuoteBloc;
late BackgroundImagesBloc _backgroundImagesBloc;
@override
void initState() {
super.initState();
_dailyQuoteBloc = context.read<QuoteBloc>();
_backgroundImagesBloc = context.read<BackgroundImagesBloc>();
// get daily quote & background photos
_dailyQuoteBloc.add(const GetQuoteEvent());
_backgroundImagesBloc.add(const GetBackgroundImagesEvent());
}
@override
Widget build(BuildContext context) {
final header = Padding(
padding: const EdgeInsets.fromLTRB(24, 8, 16, 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text('Kuwot', style: GoogleFonts.sriracha(fontSize: 30)),
const SizedBox(width: 2),
SvgPicture.asset(
'assets/svgs/chat-quote.svg',
height: 24,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurface,
BlendMode.srcIn,
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
onPressed: () {
context.router.push(const DonationRoute());
},
icon: const FaIcon(FontAwesomeIcons.mugHot),
),
IconButton(
onPressed: () {
context.router.push(const AppSettingsRoute());
},
icon: const FaIcon(FontAwesomeIcons.sliders),
),
],
),
],
),
);
final body = SafeArea(
child: Stack(
fit: StackFit.expand,
children: [
Align(alignment: Alignment.topLeft, child: header),
Padding(
padding: const EdgeInsets.fromLTRB(24, 70, 24, 24),
child: _buildQuote(),
),
],
),
);
return Scaffold(body: body);
}
Widget _buildQuote() {
final screenshotEnabledQuote = Screenshot(
controller: _screenshotController,
child: Stack(
fit: StackFit.expand,
children: [
BackgroundPhotoWidget(
backgroundIndex: _backgroundIndex,
hideImageInfoButton: _isSharingQuote,
),
const Align(alignment: Alignment.center, child: QuoteWidget()),
],
),
);
final actionButtons = [
Expanded(
child: _buildQuoteActionButton(
onPressed: () {
_dailyQuoteBloc.add(const GetQuoteEvent());
},
icon: const FaIcon(FontAwesomeIcons.quoteRight),
),
),
Expanded(
child: _buildQuoteActionButton(
onPressed: _cycleBackground,
icon: const FaIcon(FontAwesomeIcons.image),
),
),
Expanded(
child: _buildQuoteActionButton(
onPressed: () async {
final result = await showAdaptiveDialog<TranslationTarget?>(
context: context,
barrierDismissible: true,
builder: (context) => const TranslateTargetDialog(),
);
if (result != null) {
_dailyQuoteBloc.add(GetTranslatedQuoteEvent(result));
}
},
icon: const FaIcon(FontAwesomeIcons.language),
),
),
Expanded(
child: _buildQuoteActionButton(
onPressed: _shareQuote,
icon: const FaIcon(FontAwesomeIcons.shareNodes),
),
),
];
return Card(
clipBehavior: Clip.antiAlias,
margin: const EdgeInsets.all(0),
elevation: 12,
child: Column(
children: [
Expanded(child: screenshotEnabledQuote),
Row(children: actionButtons),
],
),
);
}
Widget _buildQuoteActionButton({
required VoidCallback onPressed,
required Widget icon,
}) {
return InkWell(
onTap: onPressed,
child: Container(
height: 50,
alignment: Alignment.center,
padding: const EdgeInsets.all(8),
child: icon,
),
);
}
void _cycleBackground() {
setState(() {
_backgroundIndex = ++_backgroundIndex % imagesPerPage;
});
}
Future<void> _shareQuote() async {
setState(() => _isSharingQuote = true);
final image = await _screenshotController.capture();
setState(() => _isSharingQuote = false);
if (image == null) return;
final shareParams = ShareParams(
files: [
XFile.fromData(
image,
mimeType: 'image/png',
name: 'kuwot_${DateTime.now().millisecondsSinceEpoch}.png',
),
],
);
await SharePlus.instance.share(shareParams);
}
}

View file

@ -0,0 +1,134 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:kuwot/features/quote/domain/entities/background_image.dart';
import 'package:url_launcher/url_launcher_string.dart';
class AboutImageDialog extends StatelessWidget {
final BackgroundImage image;
const AboutImageDialog({required this.image, super.key});
@override
Widget build(BuildContext context) {
final header = Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 8, 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('About Image', style: GoogleFonts.sriracha(fontSize: 24)),
IconButton(
onPressed: () {
context.router.maybePop();
},
icon: const FaIcon(FontAwesomeIcons.xmark),
),
],
),
);
final info = Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
CircleAvatar(
radius: 40,
child: ClipOval(
child: CachedNetworkImage(
imageUrl: image.authorProfileImageUrl,
placeholder: (context, url) => CircularProgressIndicator(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
),
const SizedBox(height: 8),
Text(
image.authorName,
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
Text(image.authorBio, textAlign: TextAlign.center),
const SizedBox(height: 4),
Wrap(
alignment: WrapAlignment.center,
children: [
_buildUserInfoChip(
icon: FontAwesomeIcons.locationDot,
text: image.authorLocation,
),
_buildUserInfoChip(
icon: FontAwesomeIcons.cameraRetro,
text: '${image.authorTotalPhotos} photos',
),
_buildUserInfoChip(
icon: FontAwesomeIcons.thumbsUp,
text: '${image.authorTotalLikes} likes',
),
image.authorIsForHire
? _buildUserInfoChip(
icon: FontAwesomeIcons.handshake,
text: 'For hire',
)
: const SizedBox.shrink(),
],
),
const SizedBox(height: 16),
Text(
image.description,
style: Theme.of(
context,
).textTheme.bodyLarge?.copyWith(fontStyle: FontStyle.italic),
),
],
),
);
final footer = Padding(
padding: const EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
launchUrlString(image.authorUrl);
},
child: const Text('Author profile'),
),
TextButton(
onPressed: () {
launchUrlString(image.originUrl);
},
child: const Text('Original image'),
),
],
),
);
return Dialog(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [header, const Divider(height: 1), info, footer],
),
);
}
Widget _buildUserInfoChip({required IconData icon, required String text}) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
FaIcon(icon, size: 16),
const SizedBox(width: 4),
Flexible(child: Text(text)),
],
),
);
}
}

View file

@ -0,0 +1,152 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:kuwot/core/presentation/error_retry_snackbar.dart';
import 'package:kuwot/features/quote/data/data_sources/remote/kuwot_api_remote_data_source.dart';
import 'package:kuwot/features/quote/presentation/bloc/background_images_bloc.dart';
import 'package:kuwot/features/quote/presentation/widgets/about_image_dialog.dart';
import 'package:kuwot/utilities.dart';
import 'package:url_launcher/url_launcher_string.dart';
class BackgroundPhotoWidget extends StatelessWidget {
final int backgroundIndex;
final bool hideImageInfoButton;
const BackgroundPhotoWidget({
required this.backgroundIndex,
this.hideImageInfoButton = false,
super.key,
}) : assert(backgroundIndex < imagesPerPage);
@override
Widget build(BuildContext context) {
return BlocConsumer<BackgroundImagesBloc, BackgroundImagesState>(
listener: (context, state) {
// handle error state
if (state is BackgroundImagesErrorState) {
ErrorRetrySnackbar.show(
context,
errorMessage: state.message,
onRetry: () {
context.read<BackgroundImagesBloc>().add(
const GetBackgroundImagesEvent(),
);
},
);
}
},
builder: (context, state) {
if (state is BackgroundImagesLoadedState) {
const borderRadius = BorderRadius.only(
bottomLeft: Radius.circular(16),
);
final imageInfoButton = Material(
color: Colors.black26,
borderRadius: borderRadius,
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () {
showAdaptiveDialog(
context: context,
barrierDismissible: true,
builder: (context) => AboutImageDialog(
image: state.backgroundImages[backgroundIndex],
),
);
},
child: Container(
height: 40,
width: 40,
alignment: Alignment.center,
decoration: const BoxDecoration(borderRadius: borderRadius),
child: const FaIcon(
FontAwesomeIcons.circleInfo,
size: 20,
color: Colors.white54,
),
),
),
);
final imageAttribution = ColoredBox(
color: Colors.black38,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Wrap(
alignment: WrapAlignment.center,
children: [
Text(
'Photo by ',
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: Colors.white54),
),
GestureDetector(
child: Text(
state.backgroundImages[backgroundIndex].authorName,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
decoration: TextDecoration.underline,
color: Colors.white.withValues(alpha: 0.85),
decorationColor: Colors.white.withValues(alpha: 0.85),
),
),
onTap: () async {
await launchUrlString(
state.backgroundImages[backgroundIndex].authorUrl,
);
},
),
Text(
' on ',
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: Colors.white54),
),
GestureDetector(
child: Text(
'Unsplash',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
decoration: TextDecoration.underline,
color: Colors.white.withValues(alpha: 0.85),
decorationColor: Colors.white.withValues(alpha: 0.85),
),
),
onTap: () async {
await launchUrlString(
"https://unsplash.com?utm_source=kuwot&utm_medium=referral",
);
},
),
],
),
),
);
return Stack(
fit: StackFit.expand,
children: [
CachedNetworkImage(
imageUrl: state.backgroundImages[backgroundIndex].url,
fit: BoxFit.cover,
placeholder: (_, _) {
final avgColor =
state.backgroundImages[backgroundIndex].color;
return Container(color: getColorFromHexString(avgColor));
},
errorWidget: (_, _, _) {
return Container(color: Colors.grey);
},
),
hideImageInfoButton
? const SizedBox()
: Positioned(top: 0, right: 0, child: imageInfoButton),
Positioned(bottom: 0, left: 0, right: 0, child: imageAttribution),
],
);
}
return Container(color: Colors.grey);
},
);
}
}

View file

@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:kuwot/core/presentation/error_retry_snackbar.dart';
import 'package:kuwot/features/quote/presentation/bloc/quote_bloc.dart';
class QuoteWidget extends StatefulWidget {
const QuoteWidget({super.key});
@override
State<QuoteWidget> createState() => _QuoteWidgetState();
}
class _QuoteWidgetState extends State<QuoteWidget>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
String? _quoteBody;
String? _quoteAuthor;
@override
void initState() {
super.initState();
// init animation
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_animationController.addListener(() => setState(() {}));
}
@override
Widget build(BuildContext context) {
return BlocConsumer<QuoteBloc, QuoteState>(
listener: (context, state) {
// animate quote loading state
if (state is QuoteLoadingState) {
_animationController.repeat(reverse: true);
} else {
_animationController.forward();
}
// handle error state
if (state is QuoteErrorState) {
ErrorRetrySnackbar.show(
context,
errorMessage: state.message,
onRetry: () {
context.read<QuoteBloc>().add(const GetQuoteEvent());
},
);
}
},
builder: (context, state) {
if (state is QuoteLoadedState) {
_quoteBody = state.quote.body;
_quoteAuthor = state.quote.author;
}
return Container(
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.all(16.0),
margin: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Opacity(
opacity: _animationController.value,
child: SvgPicture.asset(
'assets/svgs/chat-quote-fill.svg',
height: 54,
colorFilter: const ColorFilter.mode(
Colors.white54,
BlendMode.srcIn,
),
),
),
const SizedBox(height: 30),
Text(
_quoteBody ?? '...',
style: Theme.of(
context,
).textTheme.headlineSmall?.copyWith(color: Colors.white),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
Text(
'- ${_quoteAuthor ?? 'Kuwot'}',
style: Theme.of(
context,
).textTheme.bodyLarge?.copyWith(color: Colors.white),
textAlign: TextAlign.right,
),
],
),
);
},
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
}

View file

@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:kuwot/core/data/local/translation_target_config.dart';
import 'package:kuwot/core/presentation/bloc/config/translation_target_cubit.dart';
import 'package:kuwot/features/quote/presentation/bloc/translations_bloc.dart';
class TranslateTargetDialog extends StatefulWidget {
const TranslateTargetDialog({super.key});
@override
State<TranslateTargetDialog> createState() => _TranslateTargetDialogState();
}
class _TranslateTargetDialogState extends State<TranslateTargetDialog> {
@override
void initState() {
super.initState();
// load translations
context.read<TranslationsBloc>().add(const GetTranslationsEvent());
}
@override
Widget build(BuildContext context) {
final header = Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 8, 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Quote Language', style: GoogleFonts.sriracha(fontSize: 24)),
IconButton(
onPressed: () {
Navigator.of(context).pop<TranslationTarget?>(null);
},
icon: const FaIcon(FontAwesomeIcons.xmark),
),
],
),
);
final currentTranslationTarget = context
.read<TranslationTargetCubit>()
.state;
final languageList = SizedBox(
height: MediaQuery.of(context).size.height / 2,
child: BlocBuilder<TranslationsBloc, TranslationsState>(
builder: (context, state) {
if (state is TranslationsLoadedState) {
final translations = state.translations;
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: translations.length,
itemBuilder: (context, index) {
final isSelected =
currentTranslationTarget.id == translations[index].id;
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
title: Text(
translations[index].language,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: isSelected
? FontWeight.bold
: FontWeight.normal,
),
),
trailing: isSelected
? const FaIcon(FontAwesomeIcons.check)
: null,
onTap: () {
final translationTarget = TranslationTarget(
id: translations[index].id,
name: translations[index].language,
);
context.read<TranslationTargetCubit>().set(
translationTarget,
);
Navigator.of(
context,
).pop<TranslationTarget?>(translationTarget);
},
);
},
);
}
return const Center(child: CircularProgressIndicator());
},
),
);
return Dialog(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [header, const Divider(height: 1), languageList],
),
);
}
}