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,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],
),
);
}
}