348 lines
11 KiB
Dart
348 lines
11 KiB
Dart
import 'package:epic_story_app/domain/entities/book_entity.dart';
|
|
import 'package:epic_story_app/domain/entities/book_histories_entity.dart';
|
|
import 'package:epic_story_app/feature/others/main_controller/main_controller.dart';
|
|
import 'package:epic_story_app/feature/rewards/reward_home/components/reward_claim_button.dart';
|
|
import 'package:epic_story_app/feature/rewards/reward_home/reward_home_controller.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
|
|
class BookReward extends StatelessWidget {
|
|
const BookReward({super.key, required this.controller});
|
|
|
|
final RewardHomeController controller;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Obx(() {
|
|
if (controller.isLoading.value) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
|
|
final books = controller.books;
|
|
if (books.isEmpty) {
|
|
return ListView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 18),
|
|
children: const [
|
|
SizedBox(height: 80),
|
|
Center(
|
|
child: Text(
|
|
'Belum ada hadiah buku yang tersedia.',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.black54,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
return ListView.separated(
|
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 18),
|
|
itemCount: books.length,
|
|
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
|
itemBuilder: (context, index) {
|
|
final book = books[index];
|
|
final history = controller.historyForBook(book.bookId);
|
|
return GestureDetector(
|
|
onTap: () => controller.goToBookRead(book),
|
|
child: _BookRewardCard(
|
|
itemIndex: index,
|
|
book: book,
|
|
history: history,
|
|
onClaim: () => controller.openBookRewardClaim(book),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
class _BookRewardCard extends StatefulWidget {
|
|
const _BookRewardCard({
|
|
required this.itemIndex,
|
|
required this.book,
|
|
required this.history,
|
|
required this.onClaim,
|
|
});
|
|
|
|
final int itemIndex;
|
|
final BookEntity book;
|
|
final BookHistoryEntity? history;
|
|
final VoidCallback onClaim;
|
|
|
|
@override
|
|
State<_BookRewardCard> createState() => _BookRewardCardState();
|
|
}
|
|
|
|
class _BookRewardCardState extends State<_BookRewardCard>
|
|
with SingleTickerProviderStateMixin {
|
|
final MainController mainController = Get.find<MainController>();
|
|
|
|
late final AnimationController _btnScaleController;
|
|
late final Animation<double> _btnScale;
|
|
bool _animating = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_btnScaleController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 950),
|
|
);
|
|
_btnScale = Tween<double>(begin: 0.96, end: 1.04).animate(
|
|
CurvedAnimation(parent: _btnScaleController, curve: Curves.easeInOut),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_btnScaleController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _syncButtonAnimation(bool shouldAnimate) {
|
|
if (shouldAnimate && !_animating) {
|
|
_btnScaleController.repeat(reverse: true);
|
|
_animating = true;
|
|
} else if (!shouldAnimate && _animating) {
|
|
_btnScaleController.stop();
|
|
_btnScaleController.value = 0.5;
|
|
_animating = false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final book = widget.book;
|
|
final history = widget.history;
|
|
final isColorOne = widget.itemIndex.isEven;
|
|
|
|
final title = history?.title ?? book.title ?? 'Judul Buku';
|
|
final summary = book.summary ?? '-';
|
|
final category = history?.category ?? book.category ?? '-';
|
|
final totalPages = history?.totalPages ?? book.totalPages ?? 0;
|
|
final isFinished = history?.isFinished == true;
|
|
final isClaimed = (history?.claimedRewards?.isNotEmpty ?? false);
|
|
|
|
final rewardImages = book.rewards ?? [];
|
|
final rewardsClaimed = history?.claimedRewards ?? [];
|
|
final claimedThumbs = rewardsClaimed
|
|
.where((i) => i >= 0 && i < rewardImages.length)
|
|
.map((i) => rewardImages[i])
|
|
.take(3)
|
|
.toList();
|
|
|
|
final shouldAnimateButton = !isClaimed && isFinished;
|
|
_syncButtonAnimation(shouldAnimateButton);
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: isColorOne
|
|
? const [Color(0xFFFFDAA0), Color(0xFFFFFFFF)]
|
|
: const [Color(0xFFAAFFC0), Color(0xFFFFFFFF)],
|
|
stops: const [0.0, 1.0],
|
|
),
|
|
borderRadius: BorderRadius.circular(18),
|
|
border: Border.all(color: Colors.black.withOpacity(0.6), width: 1.1),
|
|
),
|
|
padding: const EdgeInsets.all(12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_BookCover(url: history?.bookCover ?? book.bookCover),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.w800,
|
|
color: Colors.black,
|
|
height: 1.1,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Category : $category',
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
color: Colors.black87,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
'Total Halaman : $totalPages',
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
color: Colors.black87,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
summary,
|
|
maxLines: 3,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
color: Colors.black87,
|
|
height: 1.25,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
if (claimedThumbs.isNotEmpty)
|
|
Row(
|
|
children: List.generate(claimedThumbs.length, (i) {
|
|
final isLast = i == claimedThumbs.length - 1;
|
|
return Padding(
|
|
padding: EdgeInsets.only(right: isLast ? 0 : 10),
|
|
child: _Thumb(
|
|
url: claimedThumbs[i],
|
|
onTap: () {
|
|
mainController.showRewardPhotoViewer(
|
|
imageUrls: claimedThumbs,
|
|
initialIndex: i,
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
if (shouldAnimateButton) ...[
|
|
const SizedBox(height: 3),
|
|
ScaleTransition(
|
|
scale: _btnScale,
|
|
child: RewardClaimButton(onPressed: widget.onClaim),
|
|
),
|
|
],
|
|
const SizedBox(height: 10),
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
isClaimed ? 'Hadiah sudah diklaim' : 'Hadiah belum diklaim',
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.black,
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
Image.asset(
|
|
isClaimed
|
|
? 'assets/images/reward-claimed.png'
|
|
: 'assets/images/reward-not-claimed.png',
|
|
width: 16,
|
|
height: 16,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _BookCover extends StatelessWidget {
|
|
const _BookCover({required this.url});
|
|
|
|
final String? url;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
width: 110,
|
|
height: 150,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: Colors.black.withOpacity(0.6), width: 1.0),
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(14),
|
|
child: url == null
|
|
? const Center(
|
|
child: Text(
|
|
'Cover Buku',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
)
|
|
: Image.network(
|
|
url!,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (context, error, stackTrace) => const Center(
|
|
child:
|
|
Icon(Icons.broken_image_outlined, color: Colors.black54),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Thumb extends StatelessWidget {
|
|
const _Thumb({required this.url, required this.onTap});
|
|
|
|
final String? url;
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Container(
|
|
width: 72,
|
|
height: 72,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border:
|
|
Border.all(color: Colors.black.withOpacity(0.6), width: 1.0),
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(10),
|
|
child: url == null
|
|
? const Icon(Icons.image_outlined, color: Colors.black54)
|
|
: Image.network(
|
|
url!,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (context, error, stackTrace) => const Icon(
|
|
Icons.broken_image_outlined,
|
|
color: Colors.black54,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|