feat: add favorites, AI tutors, and references features
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
1ab2705f46
commit
5e698ae78c
|
|
@ -0,0 +1,9 @@
|
|||
import 'package:epic_story_app/feature/ai_tutors/presentation/ai_tutor_controller.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class AiTutorBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut(() => AiTutorController());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import 'package:get/get.dart';
|
||||
|
||||
class AiTutorController extends GetxController{
|
||||
//
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import 'package:epic_story_app/core/constants/size/epic_size.dart';
|
||||
import 'package:epic_story_app/feature/ai_tutors/presentation/ai_tutor_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class AiTutorPage extends StatelessWidget {
|
||||
const AiTutorPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
final AiTutorController controller = Get.find();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.purple.withOpacity(0.4),
|
||||
body: Center(
|
||||
child: Text(
|
||||
"AI Tutor Home",
|
||||
style: EpicSize.headlineLarge,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
class CreateCollectionArgs {
|
||||
final bool isEdit;
|
||||
final String type;
|
||||
final String collectionId;
|
||||
final String? title;
|
||||
final String? desc;
|
||||
final int? bgColor;
|
||||
|
||||
const CreateCollectionArgs.edit({
|
||||
required this.type,
|
||||
required this.collectionId,
|
||||
this.title,
|
||||
this.desc,
|
||||
this.bgColor,
|
||||
}) : isEdit = true;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import 'package:epic_story_app/data/modules/collection_module.dart';
|
||||
import 'package:epic_story_app/domain/usecases/collection_usecase.dart';
|
||||
import 'package:epic_story_app/feature/favorites/presentation/create_collection/create_collection_controller.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class CreateCollectionBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
CollectionModule();
|
||||
Get.lazyPut<CreateCollectionController>(
|
||||
() => CreateCollectionController(
|
||||
collectionUsecase: Get.find<CollectionUsecase>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
import 'package:epic_story_app/core/utils/epic_log.dart';
|
||||
import 'package:epic_story_app/data/models/remotes/collection_color_model.dart';
|
||||
import 'package:epic_story_app/domain/entities/children_collection_entity.dart';
|
||||
import 'package:epic_story_app/domain/entities/collections/flashcard_collection_entity.dart';
|
||||
import 'package:epic_story_app/domain/usecases/collection_usecase.dart';
|
||||
import 'package:epic_story_app/feature/others/main_controller/main_controller.dart';
|
||||
import 'package:epic_story_app/feature/favorites/presentation/create_collection/create_collection_args.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class CreateCollectionController extends GetxController {
|
||||
final CollectionUsecase collectionUsecase;
|
||||
|
||||
CreateCollectionController({
|
||||
required this.collectionUsecase,
|
||||
});
|
||||
|
||||
final TextEditingController nameCtrl = TextEditingController();
|
||||
final TextEditingController descCtrl = TextEditingController();
|
||||
|
||||
final mainController = Get.find<MainController>();
|
||||
|
||||
final types = const ['Buku', 'Flashcard'];
|
||||
final RxString selectedType = ''.obs;
|
||||
|
||||
final List<CollectionColorModel> palette = kCollectionColorPalette;
|
||||
|
||||
final RxInt selectedIndex = (-1).obs;
|
||||
final RxBool isSaving = false.obs;
|
||||
final RxBool isEditMode = false.obs;
|
||||
String? editingCollectionId;
|
||||
|
||||
CollectionColorModel? get selectedColorModel =>
|
||||
selectedIndex.value >= 0 ? palette[selectedIndex.value] : null;
|
||||
|
||||
Color? get selectedColor => selectedColorModel?.color;
|
||||
|
||||
LinearGradient? get selectedGradient => selectedColorModel?.gradient;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_loadArgs();
|
||||
}
|
||||
|
||||
void _loadArgs() {
|
||||
final args = Get.arguments;
|
||||
if (args is! CreateCollectionArgs || !args.isEdit) return;
|
||||
|
||||
isEditMode.value = true;
|
||||
editingCollectionId = args.collectionId;
|
||||
selectedType.value = args.type;
|
||||
nameCtrl.text = args.title ?? '';
|
||||
descCtrl.text = args.desc ?? '';
|
||||
|
||||
final colorIndex = args.bgColor;
|
||||
if (colorIndex != null && colorIndex >= 0 && colorIndex < palette.length) {
|
||||
selectedIndex.value = colorIndex;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onSubmit() async {
|
||||
final title = nameCtrl.text.trim();
|
||||
final desc = descCtrl.text.trim();
|
||||
final colorIndex = selectedIndex.value;
|
||||
final type = selectedType.value;
|
||||
|
||||
if (title.isEmpty) {
|
||||
Get.snackbar('Gagal', 'Nama koleksi tidak boleh kosong');
|
||||
return;
|
||||
}
|
||||
if (colorIndex < 0 || colorIndex >= palette.length) {
|
||||
Get.snackbar('Gagal', 'Pilih warna background terlebih dahulu');
|
||||
return;
|
||||
}
|
||||
if (type.isEmpty) {
|
||||
Get.snackbar('Gagal', 'Pilih tipe koleksi');
|
||||
return;
|
||||
}
|
||||
|
||||
isSaving.value = true;
|
||||
final now = DateTime.now();
|
||||
final generatedId = now.millisecondsSinceEpoch.toString();
|
||||
|
||||
EpicLog.debug("type selected: ${selectedType.value}");
|
||||
|
||||
try {
|
||||
mainController.showLoadingPage();
|
||||
if (isEditMode.value) {
|
||||
final collectionId = editingCollectionId;
|
||||
if (collectionId == null || collectionId.isEmpty) {
|
||||
Get.snackbar('Gagal', 'Koleksi tidak ditemukan');
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == 'Buku') {
|
||||
final updatedCollection = ChildrenCollectionEntity(
|
||||
id: collectionId,
|
||||
bgColor: colorIndex,
|
||||
title: title,
|
||||
desc: desc.isEmpty ? null : desc,
|
||||
updatedAt: now,
|
||||
);
|
||||
await collectionUsecase.updateCollection(
|
||||
collection: updatedCollection,
|
||||
);
|
||||
} else {
|
||||
final updatedCollection = FlashCardCollectionEntity(
|
||||
collectionId: collectionId,
|
||||
title: title,
|
||||
desc: desc.isEmpty ? null : desc,
|
||||
bgColor: colorIndex,
|
||||
updatedAt: now,
|
||||
);
|
||||
await collectionUsecase.updateFlashcardCollection(
|
||||
collection: updatedCollection,
|
||||
);
|
||||
}
|
||||
mainController.hideLoadingPage();
|
||||
Get.back(result: true);
|
||||
Get.snackbar('Berhasil', 'Koleksi berhasil diperbarui');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedType.value == 'Buku') {
|
||||
final bookCollection = ChildrenCollectionEntity(
|
||||
id: generatedId,
|
||||
bgColor: colorIndex,
|
||||
title: title,
|
||||
desc: desc.isEmpty ? null : desc,
|
||||
nBooks: 0,
|
||||
nPages: 0,
|
||||
bookCovers: const [],
|
||||
categories: const [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
);
|
||||
await collectionUsecase.createCollection(collection: bookCollection);
|
||||
} else {
|
||||
var flashcardCollection = FlashCardCollectionEntity(
|
||||
collectionId: generatedId,
|
||||
title: title,
|
||||
desc: desc.isEmpty ? null : desc,
|
||||
bgColor: colorIndex,
|
||||
categories: const [],
|
||||
totalCards: 0,
|
||||
totalFlashcard: 0,
|
||||
totalQuiz: 0,
|
||||
completedQuiz: 0,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
);
|
||||
await collectionUsecase.createFlashcardCollection(
|
||||
collection: flashcardCollection,
|
||||
);
|
||||
}
|
||||
mainController.hideLoadingPage();
|
||||
Get.back(result: true);
|
||||
Get.snackbar('Berhasil', 'Koleksi berhasil dibuat');
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
isEditMode.value
|
||||
? 'Tidak dapat memperbarui koleksi'
|
||||
: 'Tidak dapat membuat koleksi',
|
||||
);
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
mainController.hideLoadingPage();
|
||||
}
|
||||
}
|
||||
|
||||
void selectColor(int index) {
|
||||
selectedIndex.value = index;
|
||||
}
|
||||
|
||||
void selectType(String? value) {
|
||||
if (value != null) selectedType.value = value;
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
nameCtrl.dispose();
|
||||
descCtrl.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
import 'package:epic_story_app/core/constants/size/epic_size.dart';
|
||||
import 'package:epic_story_app/core/styles/epic_app_size.dart';
|
||||
import 'package:epic_story_app/core/widgets/cards/stacked_info_top_bar.dart';
|
||||
import 'package:epic_story_app/data/models/remotes/collection_color_model.dart';
|
||||
import 'package:epic_story_app/feature/favorites/presentation/create_collection/create_collection_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class CreateCollectionPage extends StatefulWidget {
|
||||
const CreateCollectionPage({super.key});
|
||||
|
||||
@override
|
||||
State<CreateCollectionPage> createState() => _CreateCollectionPageState();
|
||||
}
|
||||
|
||||
class _CreateCollectionPageState extends State<CreateCollectionPage> {
|
||||
late final CreateCollectionController controller =
|
||||
Get.find<CreateCollectionController>();
|
||||
|
||||
static const Color _pageBackground = Color(0xFFC6B2E6);
|
||||
static const Color _inputBorder = Color(0xFF22162F);
|
||||
static const Color _saveButton = Color(0xFF8BDE88);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: _pageBackground,
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: [
|
||||
Obx(() {
|
||||
final isEditMode = controller.isEditMode.value;
|
||||
return StackedInfoTopBar(
|
||||
titleText: isEditMode ? 'Edit Koleksi' : 'Buat Koleksi',
|
||||
infoText: isEditMode
|
||||
? 'Silahkan ubah koleksi yang\nsudah kamu buat sebelumnya'
|
||||
: 'Silahkan buat koleksi dari\nbuku/flashcard yang ingin\nkamu pelajari selanjutnya',
|
||||
isBackButtonVisible: true,
|
||||
onBackTap: () => Get.back(),
|
||||
);
|
||||
}),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
EpicSize.calculatedSize(30),
|
||||
EpicSize.calculatedSize(10),
|
||||
EpicSize.calculatedSize(30),
|
||||
EpicSize.calculatedSize(28),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: EpicAppSize.calculatedSize(16),
|
||||
),
|
||||
_FieldLabel(text: 'Nama Koleksi'),
|
||||
SizedBox(
|
||||
height: EpicAppSize.calculatedSize(10),
|
||||
),
|
||||
TextField(
|
||||
controller: controller.nameCtrl,
|
||||
decoration: _inputDecoration(),
|
||||
),
|
||||
SizedBox(
|
||||
height: EpicAppSize.calculatedSize(16),
|
||||
),
|
||||
_FieldLabel(text: 'Tipe Koleksi'),
|
||||
SizedBox(
|
||||
height: EpicAppSize.calculatedSize(10),
|
||||
),
|
||||
Obx(
|
||||
() => DropdownButtonFormField<String>(
|
||||
value: controller.selectedType.value.isEmpty
|
||||
? null
|
||||
: controller.selectedType.value,
|
||||
hint: const Text(
|
||||
'Pilih satu (dropdown)',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
items: controller.types
|
||||
.map(
|
||||
(t) => DropdownMenuItem<String>(
|
||||
value: t,
|
||||
child: Text(t),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: controller.isEditMode.value
|
||||
? null
|
||||
: controller.selectType,
|
||||
icon: const Icon(Icons.keyboard_arrow_down_rounded),
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
decoration: _inputDecoration(),
|
||||
isExpanded: true,
|
||||
dropdownColor: _pageBackground,
|
||||
),
|
||||
),
|
||||
SizedBox(height: EpicAppSize.calculatedSize(20)),
|
||||
_FieldLabel(text: 'Deskripsi Koleksi'),
|
||||
SizedBox(height: EpicAppSize.calculatedSize(10)),
|
||||
TextField(
|
||||
controller: controller.descCtrl,
|
||||
decoration: _inputDecoration(),
|
||||
minLines: 5,
|
||||
maxLines: 5,
|
||||
),
|
||||
SizedBox(height: EpicAppSize.calculatedSize(22)),
|
||||
_FieldLabel(text: 'Background Koleksi'),
|
||||
SizedBox(height: EpicAppSize.calculatedSize(10)),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final spacing = EpicSize.calculatedSize(18);
|
||||
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: controller.palette.length,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate:
|
||||
SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 5,
|
||||
mainAxisSpacing: spacing,
|
||||
crossAxisSpacing: spacing,
|
||||
childAspectRatio: 1,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final colorModel = controller.palette[index];
|
||||
return Obx(
|
||||
() {
|
||||
final selected =
|
||||
controller.selectedIndex.value == index;
|
||||
return GestureDetector(
|
||||
onTap: () => controller.selectColor(index),
|
||||
child: _ColorSwatch(
|
||||
colorModel: colorModel,
|
||||
selected: selected,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: EpicSize.calculatedSize(32)),
|
||||
Obx(
|
||||
() => controller.isSaving.value
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
SizedBox(height: EpicSize.sizeSmall),
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: controller.onSubmit,
|
||||
child: Container(
|
||||
width: EpicSize.calculatedSize(220),
|
||||
height: EpicSize.calculatedSize(52),
|
||||
decoration: BoxDecoration(
|
||||
color: _saveButton,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0xFF1A9E1A),
|
||||
width: 1.2,
|
||||
),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x3F000000),
|
||||
offset: Offset(0, 4),
|
||||
blurRadius: 6,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Simpan',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: EpicAppSize.calculatedSize(22),
|
||||
fontWeight: FontWeight.w900,
|
||||
height: 0.9,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
InputDecoration _inputDecoration() {
|
||||
return InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: EpicSize.sizeSmallMedium,
|
||||
vertical: EpicSize.sizeSmall,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.transparent,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: _inputBorder),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: _inputBorder, width: 1.5),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FieldLabel extends StatelessWidget {
|
||||
final String text;
|
||||
|
||||
const _FieldLabel({required this.text});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: Color(0xFF120D1A),
|
||||
fontSize: EpicAppSize.calculatedSize(24),
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ColorSwatch extends StatelessWidget {
|
||||
final CollectionColorModel colorModel;
|
||||
final bool selected;
|
||||
|
||||
const _ColorSwatch({required this.colorModel, required this.selected});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
width: EpicAppSize.calculatedSize(45),
|
||||
height: EpicAppSize.calculatedSize(45),
|
||||
decoration: BoxDecoration(
|
||||
gradient: colorModel.gradient,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: selected ? Colors.black : Colors.black26,
|
||||
width: selected ? 2.5 : 1.5,
|
||||
),
|
||||
boxShadow: selected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: colorModel.color.withValues(alpha: 0.35),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import 'package:epic_story_app/data/modules/book_module.dart';
|
||||
import 'package:epic_story_app/data/modules/collection_module.dart';
|
||||
import 'package:epic_story_app/domain/usecases/book_usecase.dart';
|
||||
import 'package:epic_story_app/domain/usecases/collection_usecase.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'detail_collection_controller.dart';
|
||||
|
||||
class DetailCollectionBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
CollectionModule();
|
||||
BookModule();
|
||||
Get.lazyPut<DetailCollectionController>(
|
||||
() => DetailCollectionController(
|
||||
collectionUsecase: Get.find<CollectionUsecase>(),
|
||||
bookUsecase: Get.find<BookUsecase>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
import 'package:epic_story_app/core/utils/epic_log.dart';
|
||||
import 'package:epic_story_app/core/utils/epic_snackbar.dart';
|
||||
import 'package:epic_story_app/domain/entities/collection_book_entity.dart';
|
||||
import 'package:epic_story_app/domain/entities/collection_detail_entity.dart';
|
||||
import 'package:epic_story_app/domain/usecases/book_usecase.dart';
|
||||
import 'package:epic_story_app/domain/usecases/collection_usecase.dart';
|
||||
import 'package:epic_story_app/feature/favorites/presentation/create_collection/create_collection_args.dart';
|
||||
import 'package:epic_story_app/core/routes/epic_routes.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class DetailCollectionController extends GetxController {
|
||||
var collectionId = ''.obs;
|
||||
var collectionDetail = Rx<CollectionDetailEntity?>(null);
|
||||
|
||||
var loading = false.obs;
|
||||
|
||||
final CollectionUsecase collectionUsecase;
|
||||
final BookUsecase bookUsecase;
|
||||
|
||||
DetailCollectionController({
|
||||
required this.collectionUsecase,
|
||||
required this.bookUsecase,
|
||||
});
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
collectionId.value = Get.arguments;
|
||||
await _loadDetail();
|
||||
}
|
||||
|
||||
Future<void> _loadDetail() async {
|
||||
try {
|
||||
loading.value = true;
|
||||
final detail = await collectionUsecase.getCollectionDetail(
|
||||
collectionId: collectionId.value,
|
||||
);
|
||||
collectionDetail.value = detail;
|
||||
} catch (e, s) {
|
||||
EpicLog.exception(e, s, this, '_loadDetail');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _showDeleteConfirmation({
|
||||
required String title,
|
||||
required String subtitle,
|
||||
}) async {
|
||||
final result = await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(subtitle),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFFF0000),
|
||||
),
|
||||
child: const Text('Hapus'),
|
||||
),
|
||||
],
|
||||
),
|
||||
barrierDismissible: true,
|
||||
);
|
||||
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
Future<void> deleteBookFromCollection(CollectionBookEntity book) async {
|
||||
final isConfirmed = await _showDeleteConfirmation(
|
||||
title: 'Hapus buku dari koleksi?',
|
||||
subtitle: 'Buku "${book.title ?? '-'}" akan dihapus dari koleksi ini.',
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
await collectionUsecase.removeBookFromCollection(
|
||||
collectionId: collectionId.value,
|
||||
bookId: book.id,
|
||||
);
|
||||
await _loadDetail();
|
||||
} catch (e, s) {
|
||||
EpicLog.exception(e, s, this, 'deleteBookFromCollection');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteCollection() async {
|
||||
final detail = collectionDetail.value;
|
||||
if (detail == null || detail.id.isEmpty) return;
|
||||
|
||||
final isConfirmed = await _showDeleteConfirmation(
|
||||
title: 'Hapus koleksi buku?',
|
||||
subtitle: 'Koleksi "${detail.title ?? '-'}" akan dihapus permanen.',
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
await collectionUsecase.removeCollection(
|
||||
collectionId: detail.id,
|
||||
);
|
||||
loading.value = false;
|
||||
Get.back(result: true);
|
||||
} catch (e, s) {
|
||||
EpicLog.exception(e, s, this, 'deleteCollection');
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> goToBookRead(CollectionBookEntity book) async {
|
||||
try {
|
||||
loading.value = true;
|
||||
final bookEntity = await bookUsecase.getBookById(bookId: book.id);
|
||||
if (bookEntity != null) {
|
||||
Get.toNamed(EpicRoutes.bookRead, arguments: bookEntity);
|
||||
} else {
|
||||
EpicSnackBar.showWarningSnackBar('Warning', 'Buku tidak ditemukan');
|
||||
}
|
||||
} catch (e, s) {
|
||||
EpicLog.exception(e, s, this, 'goToBookRead');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> goToEditCollection() async {
|
||||
final detail = collectionDetail.value;
|
||||
if (detail == null) return;
|
||||
|
||||
final result = await Get.toNamed(
|
||||
EpicRoutes.createCollection,
|
||||
arguments: CreateCollectionArgs.edit(
|
||||
type: 'Buku',
|
||||
collectionId: detail.id,
|
||||
title: detail.title,
|
||||
desc: detail.desc,
|
||||
bgColor: detail.bgColor,
|
||||
),
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
await _loadDetail();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,410 @@
|
|||
import 'package:epic_story_app/core/styles/epic_app_size.dart';
|
||||
import 'package:epic_story_app/core/widgets/cards/collection_top_section.dart';
|
||||
import 'package:epic_story_app/domain/entities/collection_book_entity.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'detail_collection_controller.dart';
|
||||
|
||||
double _s(double value) => EpicAppSize.calculatedSize(value);
|
||||
|
||||
class DetailCollectionPage extends StatelessWidget {
|
||||
const DetailCollectionPage({super.key});
|
||||
|
||||
static const List<Color> _itemColors = [
|
||||
Color(0xFFFFE070),
|
||||
Color(0xFFFFDAA0),
|
||||
Color(0xFFAAFFC0),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.find<DetailCollectionController>();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFC6B2E6),
|
||||
body: SafeArea(
|
||||
child: Obx(() {
|
||||
if (controller.loading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final detail = controller.collectionDetail.value;
|
||||
if (detail == null) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'Detail koleksi tidak tersedia',
|
||||
style: TextStyle(fontSize: _s(15)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final books = detail.books ?? [];
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
CollectionTopSection(
|
||||
title: 'Detail Koleksi',
|
||||
onBackTap: Get.back,
|
||||
leftIconAssetPath: 'assets/images/Book Stacks 1.png',
|
||||
mascotAssetPath: 'assets/images/icon-top-bar.png',
|
||||
yellowButtonAssetPath: 'assets/images/yellow-button.png',
|
||||
yellowButtonText: 'Book',
|
||||
titleTextSize: 39,
|
||||
yellowButtonTextSize: 36,
|
||||
yellowButtonWidth: 150,
|
||||
yellowButtonHeight: 52,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(_s(16), _s(10), _s(16), 0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
detail.title ?? 'Koleksi Buku',
|
||||
style: TextStyle(
|
||||
fontSize: _s(22),
|
||||
fontWeight: FontWeight.w900,
|
||||
color: const Color(0xFF111111),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(_s(16), _s(6), _s(16), 0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Wrap(
|
||||
spacing: _s(8),
|
||||
runSpacing: _s(8),
|
||||
children: [
|
||||
_ActionButton(
|
||||
label: 'Edit Koleksi',
|
||||
icon: Icons.edit,
|
||||
backgroundColor: const Color(0xFFFFE070),
|
||||
textColor: const Color(0xFF111111),
|
||||
onTap: controller.goToEditCollection,
|
||||
),
|
||||
_ActionButton(
|
||||
label: 'Hapus Koleksi',
|
||||
icon: Icons.delete,
|
||||
backgroundColor: const Color(0xFFFF0000),
|
||||
textColor: Colors.white,
|
||||
onTap: controller.deleteCollection,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: books.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'Belum ada buku di koleksi ini',
|
||||
style: TextStyle(
|
||||
fontSize: _s(15),
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
padding: EdgeInsets.fromLTRB(0, _s(8), 0, _s(20)),
|
||||
itemCount: books.length,
|
||||
separatorBuilder: (_, __) => SizedBox(height: _s(12)),
|
||||
itemBuilder: (_, index) {
|
||||
final book = books[index];
|
||||
final color = _itemColors[index % _itemColors.length];
|
||||
return _BookItem(
|
||||
book: book,
|
||||
backgroundColor: color,
|
||||
onTap: () => controller.goToBookRead(book),
|
||||
onDelete: () async {
|
||||
await controller.deleteBookFromCollection(book);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BookItem extends StatelessWidget {
|
||||
final CollectionBookEntity book;
|
||||
final Color backgroundColor;
|
||||
final VoidCallback? onDelete;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const _BookItem({
|
||||
required this.book,
|
||||
required this.backgroundColor,
|
||||
this.onDelete,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final totalQuiz = book.totalQuiz ?? 0;
|
||||
final completedQuiz = book.completedQuiz ?? 0;
|
||||
final totalPages = book.nPages ?? 0;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: EdgeInsets.fromLTRB(_s(12), _s(12), _s(12), _s(8)),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border: Border.all(color: Colors.white, width: _s(1)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_CoverPlaceholder(coverUrl: book.coverUrl),
|
||||
SizedBox(width: _s(10)),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
book.title ?? '-'.toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: _s(20),
|
||||
fontWeight: FontWeight.w900,
|
||||
color: const Color(0xFF111111),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: onDelete,
|
||||
child: Container(
|
||||
width: _s(28),
|
||||
height: _s(28),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFF0000),
|
||||
borderRadius: BorderRadius.circular(_s(4)),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.close_rounded,
|
||||
size: _s(22),
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: _s(4)),
|
||||
if (book.summary != null && book.summary!.isNotEmpty)
|
||||
Text(
|
||||
book.summary!,
|
||||
style: TextStyle(
|
||||
fontSize: _s(17),
|
||||
height: 1.2,
|
||||
color: const Color(0xFF1A1A1A),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: _s(10)),
|
||||
Text(
|
||||
'Category : ${(book.category != null && book.category!.isNotEmpty) ? book.category : 'Kategori buku'}',
|
||||
style: TextStyle(
|
||||
fontSize: _s(17),
|
||||
color: const Color(0xFF111111),
|
||||
),
|
||||
),
|
||||
SizedBox(height: _s(18)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: _s(8)),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: _s(8),
|
||||
vertical: _s(4),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: const Color(0xFF111111), width: _s(1)),
|
||||
color: const Color(0xFFF5E9AE),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/book-stack2.png',
|
||||
width: _s(16),
|
||||
height: _s(16),
|
||||
),
|
||||
SizedBox(width: _s(4)),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'$completedQuiz dari $totalQuiz Kuis diselesaikan',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: _s(16),
|
||||
color: const Color(0xFF111111),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: _s(8)),
|
||||
Text(
|
||||
'Total Halaman : $totalPages',
|
||||
style: TextStyle(
|
||||
fontSize: _s(16),
|
||||
fontWeight: FontWeight.w700,
|
||||
color: const Color(0xFF111111),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionButton extends StatelessWidget {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Color backgroundColor;
|
||||
final Color textColor;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const _ActionButton({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.backgroundColor,
|
||||
required this.textColor,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDisabled = onTap == null;
|
||||
final bgColor =
|
||||
isDisabled ? backgroundColor.withOpacity(0.5) : backgroundColor;
|
||||
final fgColor = isDisabled ? textColor.withOpacity(0.6) : textColor;
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(_s(12)),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: _s(12),
|
||||
vertical: _s(7),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(_s(12)),
|
||||
border: Border.all(color: const Color(0xFF111111), width: _s(1)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0x33000000),
|
||||
blurRadius: _s(6),
|
||||
offset: Offset(0, _s(3)),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: _s(16), color: fgColor),
|
||||
SizedBox(width: _s(6)),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: _s(14),
|
||||
fontWeight: FontWeight.w800,
|
||||
color: fgColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CoverPlaceholder extends StatelessWidget {
|
||||
final String? coverUrl;
|
||||
|
||||
const _CoverPlaceholder({this.coverUrl});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: _s(140),
|
||||
height: _s(140),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(_s(16)),
|
||||
border: Border.all(color: const Color(0xFF111111), width: _s(1.2)),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: coverUrl != null && coverUrl!.isNotEmpty
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(_s(14)),
|
||||
child: Image.network(
|
||||
coverUrl!,
|
||||
fit: BoxFit.cover,
|
||||
width: _s(140),
|
||||
height: _s(140),
|
||||
errorBuilder: (_, __, ___) => _fallbackText(),
|
||||
),
|
||||
)
|
||||
: _fallbackText(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _fallbackText() {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.menu_book_rounded,
|
||||
size: _s(26),
|
||||
color: const Color(0xFF6B6B6B),
|
||||
),
|
||||
SizedBox(height: _s(4)),
|
||||
Text(
|
||||
'Cover Buku',
|
||||
style: TextStyle(
|
||||
fontSize: _s(11),
|
||||
color: const Color(0xFF6B6B6B),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import 'package:epic_story_app/data/modules/collection_module.dart';
|
||||
import 'package:epic_story_app/data/modules/flashcard_module.dart';
|
||||
import 'package:epic_story_app/domain/usecases/collection_usecase.dart';
|
||||
import 'package:epic_story_app/domain/usecases/flashcard_usecase.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'detail_collection_flashcard_controller.dart';
|
||||
|
||||
class DetailCollectionFlashcardBidings extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
CollectionModule();
|
||||
FlashCardModule();
|
||||
Get.lazyPut(() => DetailCollectionFlashcardController(
|
||||
collectionUsecase: Get.find<CollectionUsecase>(),
|
||||
flashCardUsecase: Get.find<FlashCardUsecase>(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
import 'package:epic_story_app/core/routes/epic_routes.dart';
|
||||
import 'package:epic_story_app/core/utils/epic_log.dart';
|
||||
import 'package:epic_story_app/core/utils/epic_snackbar.dart';
|
||||
import 'package:epic_story_app/domain/entities/collections/flashcard_collection_detail_entity.dart';
|
||||
import 'package:epic_story_app/domain/entities/collections/flashcard_collection_detail_item_entity.dart';
|
||||
import 'package:epic_story_app/domain/usecases/collection_usecase.dart';
|
||||
import 'package:epic_story_app/domain/usecases/flashcard_usecase.dart';
|
||||
import 'package:epic_story_app/feature/favorites/presentation/create_collection/create_collection_args.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class DetailCollectionFlashcardController extends GetxController {
|
||||
var collectionId = ''.obs;
|
||||
var collectionDetail = Rx<FlashcardCollectionDetailEntity?>(null);
|
||||
|
||||
var loading = false.obs;
|
||||
|
||||
final CollectionUsecase collectionUsecase;
|
||||
final FlashCardUsecase flashCardUsecase;
|
||||
|
||||
DetailCollectionFlashcardController({
|
||||
required this.collectionUsecase,
|
||||
required this.flashCardUsecase,
|
||||
});
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
collectionId.value = Get.arguments;
|
||||
await _loadDetail();
|
||||
}
|
||||
|
||||
Future<void> _loadDetail() async {
|
||||
try {
|
||||
loading.value = true;
|
||||
final detail = await collectionUsecase.getFlashcardCollectionDetail(
|
||||
collectionId: collectionId.value,
|
||||
);
|
||||
collectionDetail.value = detail;
|
||||
} catch (e, s) {
|
||||
EpicLog.exception(e, s, this, '_loadDetail');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _showDeleteConfirmation({
|
||||
required String title,
|
||||
required String subtitle,
|
||||
}) async {
|
||||
final result = await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(subtitle),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFFF0000),
|
||||
),
|
||||
child: const Text('Hapus'),
|
||||
),
|
||||
],
|
||||
),
|
||||
barrierDismissible: true,
|
||||
);
|
||||
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
Future<void> deleteBookFromCollection(
|
||||
FlashCardCollectionDetailItemEntity flashcard) async {
|
||||
final isConfirmed = await _showDeleteConfirmation(
|
||||
title: 'Hapus flashcard dari koleksi?',
|
||||
subtitle:
|
||||
'Flashcard "${flashcard.title ?? '-'}" akan dihapus dari koleksi ini.',
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
await collectionUsecase.removeFlashcardFromCollection(
|
||||
collectionId: collectionId.value,
|
||||
flashcardId: flashcard.flashcardId!,
|
||||
);
|
||||
await _loadDetail();
|
||||
} catch (e, s) {
|
||||
EpicLog.exception(e, s, this, 'deleteBookFromCollection');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteCollection() async {
|
||||
final detail = collectionDetail.value;
|
||||
final id = detail?.flashcardId ?? collectionId.value;
|
||||
if (id.isEmpty) return;
|
||||
|
||||
final isConfirmed = await _showDeleteConfirmation(
|
||||
title: 'Hapus koleksi flashcard?',
|
||||
subtitle: 'Koleksi "${detail?.title ?? '-'}" akan dihapus permanen.',
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
await collectionUsecase.removeFlashcardCollection(
|
||||
collectionId: id,
|
||||
);
|
||||
loading.value = false;
|
||||
Get.back(result: true);
|
||||
} catch (e, s) {
|
||||
EpicLog.exception(e, s, this, 'deleteCollection');
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> goToEditCollection() async {
|
||||
final detail = collectionDetail.value;
|
||||
final id = detail?.flashcardId ?? collectionId.value;
|
||||
if (id.isEmpty) return;
|
||||
|
||||
final result = await Get.toNamed(
|
||||
EpicRoutes.createCollection,
|
||||
arguments: CreateCollectionArgs.edit(
|
||||
type: 'Flashcard',
|
||||
collectionId: id,
|
||||
title: detail?.title,
|
||||
desc: detail?.desc,
|
||||
bgColor: detail?.bgColor,
|
||||
),
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
await _loadDetail();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> goToFlashcardRead({
|
||||
required String flashcardId,
|
||||
}) async {
|
||||
try {
|
||||
loading.value = true;
|
||||
final flashcard = await flashCardUsecase.getFlashcardById(
|
||||
flashcardId: flashcardId,
|
||||
);
|
||||
if (flashcard != null) {
|
||||
Get.toNamed(EpicRoutes.flashCardRead, arguments: flashcard);
|
||||
} else {
|
||||
EpicSnackBar.showWarningSnackBar("Warning", "Flashcard not found");
|
||||
}
|
||||
} catch (e, s) {
|
||||
EpicLog.exception(e, s, this, 'goToFlashcardRead');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,344 @@
|
|||
import 'package:epic_story_app/core/styles/epic_app_size.dart';
|
||||
import 'package:epic_story_app/core/widgets/cards/collection_top_section.dart';
|
||||
import 'package:epic_story_app/data/models/remotes/collection_color_model.dart';
|
||||
import 'package:epic_story_app/domain/entities/collections/flashcard_collection_detail_item_entity.dart';
|
||||
import 'package:epic_story_app/feature/favorites/presentation/detail_collection_flashcard/detail_collection_flashcard_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
double _s(double value) => EpicAppSize.calculatedSize(value);
|
||||
|
||||
class DetailCollectionFlashcardHome
|
||||
extends GetView<DetailCollectionFlashcardController> {
|
||||
const DetailCollectionFlashcardHome({super.key});
|
||||
|
||||
static const List<int> _gradientIndexes = [2, 7, 6];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFC6B2E6),
|
||||
body: SafeArea(
|
||||
child: Obx(() {
|
||||
if (controller.loading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final detail = controller.collectionDetail.value;
|
||||
final isDetailReady = detail != null;
|
||||
final items = detail?.flashcards ?? [];
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
CollectionTopSection(
|
||||
title: 'Detail Koleksi',
|
||||
onBackTap: Get.back,
|
||||
leftIconAssetPath: 'assets/images/Book Stacks 1.png',
|
||||
mascotAssetPath: 'assets/images/icon-top-bar.png',
|
||||
yellowButtonAssetPath: 'assets/images/blue-button.png',
|
||||
yellowButtonText: 'Flashcard',
|
||||
titleTextSize: 37,
|
||||
yellowButtonTextSize: 22,
|
||||
yellowButtonWidth: 172,
|
||||
yellowButtonHeight: 54,
|
||||
),
|
||||
if (detail != null)
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(_s(16), _s(10), _s(16), 0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
detail.title ?? 'Koleksi Flashcard',
|
||||
style: TextStyle(
|
||||
fontSize: _s(22),
|
||||
fontWeight: FontWeight.w900,
|
||||
color: const Color(0xFF111111),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(_s(16), _s(6), _s(16), 0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Wrap(
|
||||
spacing: _s(8),
|
||||
runSpacing: _s(8),
|
||||
children: [
|
||||
_ActionButton(
|
||||
label: 'Edit Koleksi',
|
||||
icon: Icons.edit,
|
||||
backgroundColor: const Color(0xFFAAFFC0),
|
||||
textColor: const Color(0xFF111111),
|
||||
onTap: isDetailReady
|
||||
? controller.goToEditCollection
|
||||
: null,
|
||||
),
|
||||
_ActionButton(
|
||||
label: 'Hapus Koleksi',
|
||||
icon: Icons.delete,
|
||||
backgroundColor: const Color(0xFFFF0000),
|
||||
textColor: Colors.white,
|
||||
onTap:
|
||||
isDetailReady ? controller.deleteCollection : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: items.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'Belum ada flashcard dalam koleksi ini.',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
padding:
|
||||
EdgeInsets.fromLTRB(_s(16), _s(12), _s(16), _s(16)),
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
final palette = collectionColorAt(
|
||||
_gradientIndexes[index % _gradientIndexes.length],
|
||||
);
|
||||
|
||||
return _FlashcardRow(
|
||||
item: item,
|
||||
gradient: palette.gradient,
|
||||
controller: controller,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => SizedBox(height: _s(12)),
|
||||
itemCount: items.length,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FlashcardRow extends StatelessWidget {
|
||||
final FlashCardCollectionDetailItemEntity item;
|
||||
final LinearGradient gradient;
|
||||
final DetailCollectionFlashcardController controller;
|
||||
|
||||
const _FlashcardRow({
|
||||
required this.item,
|
||||
required this.gradient,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
controller.goToFlashcardRead(flashcardId: item.flashcardId ?? '');
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: gradient,
|
||||
borderRadius: BorderRadius.circular(_s(20)),
|
||||
border: Border.all(color: const Color(0x99FFFFFF), width: _s(1.2)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Color(0x11000000),
|
||||
blurRadius: _s(7),
|
||||
offset: Offset(0, _s(3)),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(_s(12), _s(10), _s(12), _s(10)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.title ?? '-',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: _s(21),
|
||||
fontWeight: FontWeight.w900,
|
||||
color: const Color(0xFF111111),
|
||||
),
|
||||
),
|
||||
SizedBox(height: _s(4)),
|
||||
Text(
|
||||
item.summary ?? '-',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: _s(14),
|
||||
height: 1.35,
|
||||
color: const Color(0xFF1A1A1A),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: _s(8)),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
controller.deleteBookFromCollection(item);
|
||||
},
|
||||
child: Container(
|
||||
width: _s(28),
|
||||
height: _s(28),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFF0000),
|
||||
borderRadius: BorderRadius.circular(_s(4)),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.close_rounded,
|
||||
size: _s(22),
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: _s(9)),
|
||||
Wrap(
|
||||
spacing: _s(6),
|
||||
runSpacing: _s(6),
|
||||
children: [
|
||||
_chip(
|
||||
text: '${item.totalCards ?? 0} kartu',
|
||||
icon: Icons.folder_open_outlined,
|
||||
),
|
||||
_chip(text: item.category ?? '-'),
|
||||
_chip(
|
||||
text: '${item.completedQuiz ?? 0}/${item.totalQuiz ?? 0}',
|
||||
icon: Icons.assignment_outlined,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: _s(8)),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
fontSize: _s(12),
|
||||
color: const Color(0xFF111111),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _chip({required String text, IconData? icon}) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: _s(7), vertical: _s(3)),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.55),
|
||||
borderRadius: BorderRadius.circular(_s(6)),
|
||||
border: Border.all(color: const Color(0xFF111111), width: _s(0.9)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(icon, size: _s(13), color: const Color(0xFF111111)),
|
||||
SizedBox(width: _s(4)),
|
||||
],
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: _s(13),
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xFF111111),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionButton extends StatelessWidget {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Color backgroundColor;
|
||||
final Color textColor;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const _ActionButton({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.backgroundColor,
|
||||
required this.textColor,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDisabled = onTap == null;
|
||||
final bgColor =
|
||||
isDisabled ? backgroundColor.withOpacity(0.5) : backgroundColor;
|
||||
final fgColor = isDisabled ? textColor.withOpacity(0.6) : textColor;
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(_s(12)),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: _s(12),
|
||||
vertical: _s(7),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(_s(12)),
|
||||
border: Border.all(color: const Color(0xFF111111), width: _s(1)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0x33000000),
|
||||
blurRadius: _s(6),
|
||||
offset: Offset(0, _s(3)),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: _s(16), color: fgColor),
|
||||
SizedBox(width: _s(6)),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: _s(14),
|
||||
fontWeight: FontWeight.w800,
|
||||
color: fgColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import 'package:epic_story_app/feature/favorites/presentation/favorite_home/components/collection_card.dart';
|
||||
import 'package:epic_story_app/feature/favorites/presentation/favorite_home/favorite_home_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class BookCollection extends StatelessWidget {
|
||||
final FavoriteHomeController controller;
|
||||
|
||||
const BookCollection({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(() {
|
||||
final items = controller.collections;
|
||||
|
||||
if (items.isEmpty) {
|
||||
return ColoredBox(
|
||||
color: const Color(0xFFC6B2E6),
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 20),
|
||||
children: const [
|
||||
SizedBox(height: 80),
|
||||
Center(
|
||||
child: Text(
|
||||
'Belum ada koleksi buku',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ColoredBox(
|
||||
color: const Color(0xFFC6B2E6),
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 20),
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 14),
|
||||
itemBuilder: (_, index) => CollectionCard(
|
||||
item: items[index],
|
||||
index: index,
|
||||
onTap: () {
|
||||
controller.goToCollectionBookDetail(items[index]);
|
||||
},
|
||||
onDelete: () {
|
||||
controller.deleteBookCollection(items[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
import 'package:epic_story_app/data/models/remotes/collection_color_model.dart';
|
||||
import 'package:epic_story_app/domain/entities/children_collection_entity.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CollectionCard extends StatelessWidget {
|
||||
final ChildrenCollectionEntity item;
|
||||
final int index;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onDelete;
|
||||
|
||||
const CollectionCard({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.index,
|
||||
required this.onTap,
|
||||
this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final categories = item.categories ?? const [];
|
||||
final visibleCategories = categories.isNotEmpty
|
||||
? categories.take(2).toList()
|
||||
: const ['Kategori 1'];
|
||||
final baseColor = collectionColorAt(item.bgColor ?? index).color;
|
||||
final cardColor = Color.lerp(baseColor, Colors.white, 0.45)!;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 10),
|
||||
decoration: BoxDecoration(
|
||||
color: cardColor,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.white, width: 1.2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: baseColor.withValues(alpha: 0.25),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_CoverStack(covers: item.bookCovers, accentColor: baseColor),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.title ?? 'Nama Koleksi Buku',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Color(0xFF111111),
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
item.desc ??
|
||||
'Deskripsi koleksi belum tersedia untuk saat ini.',
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF1D1D1D),
|
||||
height: 1.25,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: visibleCategories
|
||||
.map((cat) => _MetaChip(text: cat))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_DeleteBadge(onTap: onDelete),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_TotalBooksBadge(total: item.nBooks ?? 0),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CoverStack extends StatelessWidget {
|
||||
final List<String>? covers;
|
||||
final Color accentColor;
|
||||
|
||||
const _CoverStack({
|
||||
this.covers,
|
||||
required this.accentColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = (covers ?? const <String>[])
|
||||
.where((cover) => cover.trim().isNotEmpty)
|
||||
.toList();
|
||||
final displayed = items.take(3).toList();
|
||||
final count = displayed.isEmpty ? 1 : displayed.length;
|
||||
|
||||
return SizedBox(
|
||||
width: 110,
|
||||
height: 128,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
for (int i = 0; i < count; i++)
|
||||
Positioned(
|
||||
left: i * 7,
|
||||
top: i * 3,
|
||||
child: _CoverBox(
|
||||
width: 94,
|
||||
height: 118,
|
||||
url: displayed.isEmpty
|
||||
? null
|
||||
: displayed[displayed.length - 1 - i],
|
||||
opacity: 1 - (i * 0.15),
|
||||
accentColor: accentColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CoverBox extends StatelessWidget {
|
||||
final double width;
|
||||
final double height;
|
||||
final String? url;
|
||||
final double opacity;
|
||||
final Color accentColor;
|
||||
|
||||
const _CoverBox({
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.url,
|
||||
required this.opacity,
|
||||
required this.accentColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Opacity(
|
||||
opacity: opacity,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.84),
|
||||
border: Border.all(color: const Color(0xFF111111), width: 1.2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: accentColor.withValues(alpha: 0.18),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: url != null
|
||||
? Image.network(
|
||||
url!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => _placeholder(),
|
||||
loadingBuilder: (context, child, progress) {
|
||||
if (progress == null) return child;
|
||||
return _placeholder(isLoading: true);
|
||||
},
|
||||
)
|
||||
: _placeholder(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _placeholder({bool isLoading = false}) {
|
||||
return Center(
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
height: 18,
|
||||
width: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.menu_book_rounded,
|
||||
size: 26,
|
||||
color: Color(0xFF6B6B6B),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'Cover Buku',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF6B6B6B),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MetaChip extends StatelessWidget {
|
||||
final String text;
|
||||
|
||||
const _MetaChip({required this.text});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFF111111), width: 1),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF111111),
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TotalBooksBadge extends StatelessWidget {
|
||||
final int total;
|
||||
|
||||
const _TotalBooksBadge({required this.total});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
border: Border.all(color: const Color(0xFF111111), width: 1),
|
||||
color: Colors.transparent,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.menu_book_rounded,
|
||||
size: 16,
|
||||
color: Color(0xFFA35BE2),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Total Buku : $total',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF111111),
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeleteBadge extends StatelessWidget {
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const _DeleteBadge({this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFF0000),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close_rounded,
|
||||
size: 22,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
import 'package:epic_story_app/data/models/remotes/collection_color_model.dart';
|
||||
import 'package:epic_story_app/domain/entities/collections/flashcard_collection_entity.dart';
|
||||
import 'package:epic_story_app/feature/favorites/presentation/favorite_home/favorite_home_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class FlashcardCollection extends StatelessWidget {
|
||||
const FlashcardCollection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.find<FavoriteHomeController>();
|
||||
|
||||
return Obx(() {
|
||||
final items = controller.flashcardCollections;
|
||||
if (items.isEmpty) {
|
||||
return ColoredBox(
|
||||
color: const Color(0xFFC6B2E6),
|
||||
child: ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 20),
|
||||
children: const [
|
||||
SizedBox(height: 80),
|
||||
Center(
|
||||
child: Text(
|
||||
'Belum ada koleksi flashcard',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ColoredBox(
|
||||
color: const Color(0xFFC6B2E6),
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 20),
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return _FlashcardCollectionCard(
|
||||
item: item,
|
||||
index: index,
|
||||
controller: controller,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 14),
|
||||
itemCount: items.length,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _FlashcardCollectionCard extends StatelessWidget {
|
||||
final FlashCardCollectionEntity item;
|
||||
final int index;
|
||||
final FavoriteHomeController controller;
|
||||
|
||||
const _FlashcardCollectionCard({
|
||||
required this.item,
|
||||
required this.controller,
|
||||
required this.index,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final categories = item.categories ?? const [];
|
||||
final totalCards = item.totalCards ?? 0;
|
||||
final totalFlashcard = item.totalFlashcard ?? 0;
|
||||
final paletteColor = collectionColorAt(item.bgColor ?? index);
|
||||
final topColor =
|
||||
Color.lerp(paletteColor.gradient.colors.last, Colors.white, 0.35)!;
|
||||
final coverAsset =
|
||||
_resolveCategoryImage(categories.isNotEmpty ? categories.first : null);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
controller.goToCollectionFlashcardDetail(item);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 14),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
topColor,
|
||||
Colors.white,
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.white, width: 1.2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: paletteColor.color.withValues(alpha: 0.25),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(65, 2, 36, 0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
item.title ?? 'Nama Koleksi Flashcard',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Color(0xFF101010),
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
controller.deleteFlashcardCollection(item);
|
||||
},
|
||||
child: Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFF0000),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close_rounded,
|
||||
size: 22,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.84),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(
|
||||
color: paletteColor.color,
|
||||
width: 3,
|
||||
),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Image.asset(
|
||||
coverAsset,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
item.desc ??
|
||||
'Deskripsi koleksi belum tersedia untuk saat ini.',
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
height: 1.25,
|
||||
color: Color(0xFF1F1F1F),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
_chip(
|
||||
icon: Icons.collections_bookmark_outlined,
|
||||
text:
|
||||
'${totalCards > 0 ? totalCards : totalFlashcard} kartu',
|
||||
),
|
||||
_chip(
|
||||
text: categories.isNotEmpty
|
||||
? categories.first
|
||||
: 'Lainnya',
|
||||
),
|
||||
Text(
|
||||
'- ${totalFlashcard}x dibaca',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF212121),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _chip({required String text, IconData? icon}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFF111111), width: 1),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(icon, size: 14, color: const Color(0xFF111111)),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF111111),
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _resolveCategoryImage(String? category) {
|
||||
final normalized = (category ?? '').toLowerCase();
|
||||
|
||||
if (normalized.contains('matematika') ||
|
||||
normalized.contains('ipa') ||
|
||||
normalized.contains('sains')) {
|
||||
return 'assets/images/flashcard-matematika.png';
|
||||
}
|
||||
if (normalized.contains('bahasa') || normalized.contains('indo')) {
|
||||
return 'assets/images/flashcard-bhs-indo.png';
|
||||
}
|
||||
|
||||
return index.isEven
|
||||
? 'assets/images/flashcard-bhs-indo.png'
|
||||
: 'assets/images/flashcard-matematika.png';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import 'package:epic_story_app/data/modules/collection_module.dart';
|
||||
import 'package:epic_story_app/domain/usecases/collection_usecase.dart';
|
||||
import 'package:epic_story_app/feature/favorites/presentation/favorite_home/favorite_home_controller.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class FavoriteHomeBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
CollectionModule();
|
||||
Get.lazyPut(() => FavoriteHomeController(
|
||||
collectionUsecase: Get.find<CollectionUsecase>(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
import 'package:epic_story_app/core/routes/epic_routes.dart';
|
||||
import 'package:epic_story_app/core/utils/epic_log.dart';
|
||||
import 'package:epic_story_app/domain/entities/children_collection_entity.dart';
|
||||
import 'package:epic_story_app/domain/entities/collections/flashcard_collection_entity.dart';
|
||||
import 'package:epic_story_app/domain/usecases/collection_usecase.dart';
|
||||
import 'package:epic_story_app/feature/others/main_controller/main_controller.dart';
|
||||
import 'package:epic_story_app/feature/utils/navigation/epic_navigation_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class FavoriteHomeController extends GetxController {
|
||||
var collections = <ChildrenCollectionEntity>[].obs;
|
||||
var flashcardCollections = <FlashCardCollectionEntity>[].obs;
|
||||
|
||||
final mainController = Get.find<MainController>();
|
||||
final navController = Get.find<EpicNavigationController>();
|
||||
|
||||
var isBook = true.obs;
|
||||
|
||||
final CollectionUsecase collectionUsecase;
|
||||
|
||||
FavoriteHomeController({
|
||||
required this.collectionUsecase,
|
||||
});
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
await refreshData();
|
||||
|
||||
ever(navController.refreshCollectionHome, (value) {
|
||||
refreshData(refreshRemote: true);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> refreshData({bool refreshRemote = false}) async {
|
||||
try {
|
||||
EpicLog.debug('Refreshing collections data');
|
||||
flashcardCollections.clear();
|
||||
if (refreshRemote) {
|
||||
await mainController.childrenUsecase.syncChildrenData();
|
||||
}
|
||||
var children = await mainController.childrenUsecase.getCurrentChildren();
|
||||
collections.clear();
|
||||
flashcardCollections.clear();
|
||||
collections.addAll(children?.collections ?? []);
|
||||
flashcardCollections.addAll(children?.flashcardCollections ?? []);
|
||||
} catch (ex, s) {
|
||||
EpicLog.exception(ex, s, this, 'refreshData');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onRefresh() async {
|
||||
await refreshData(refreshRemote: true);
|
||||
}
|
||||
|
||||
Future<bool> _showDeleteConfirmation({
|
||||
required String title,
|
||||
required String subtitle,
|
||||
}) async {
|
||||
final result = await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(subtitle),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: const Text('Batal'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFFF0000),
|
||||
),
|
||||
child: const Text('Hapus'),
|
||||
),
|
||||
],
|
||||
),
|
||||
barrierDismissible: true,
|
||||
);
|
||||
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
Future<void> deleteBookCollection(
|
||||
ChildrenCollectionEntity data,
|
||||
) async {
|
||||
try {
|
||||
if (data.id.isEmpty) return;
|
||||
final isConfirmed = await _showDeleteConfirmation(
|
||||
title: 'Hapus koleksi buku?',
|
||||
subtitle: 'Koleksi "${data.title ?? '-'}" akan dihapus permanen.',
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
|
||||
await collectionUsecase.removeCollection(
|
||||
collectionId: data.id,
|
||||
);
|
||||
await refreshData(refreshRemote: true);
|
||||
} catch (ex, s) {
|
||||
EpicLog.exception(ex, s, this, 'deleteBookCollection');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteFlashcardCollection(
|
||||
FlashCardCollectionEntity data,
|
||||
) async {
|
||||
try {
|
||||
if (data.collectionId.isEmpty) return;
|
||||
final isConfirmed = await _showDeleteConfirmation(
|
||||
title: 'Hapus koleksi flashcard?',
|
||||
subtitle: 'Koleksi "${data.title ?? '-'}" akan dihapus permanen.',
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
|
||||
await collectionUsecase.removeFlashcardCollection(
|
||||
collectionId: data.collectionId,
|
||||
);
|
||||
await refreshData(refreshRemote: true);
|
||||
} catch (ex, s) {
|
||||
EpicLog.exception(ex, s, this, 'deleteFlashcardCollection');
|
||||
}
|
||||
}
|
||||
|
||||
void goToCreateCollection() async {
|
||||
await Get.toNamed(
|
||||
EpicRoutes.createCollection,
|
||||
);
|
||||
await refreshData(refreshRemote: true);
|
||||
}
|
||||
|
||||
void goToCollectionBookDetail(
|
||||
ChildrenCollectionEntity data,
|
||||
) async {
|
||||
await Get.toNamed(
|
||||
EpicRoutes.detailCollection,
|
||||
arguments: data.id,
|
||||
);
|
||||
await refreshData(refreshRemote: true);
|
||||
}
|
||||
|
||||
void goToCollectionFlashcardDetail(
|
||||
FlashCardCollectionEntity data,
|
||||
) async {
|
||||
await Get.toNamed(
|
||||
EpicRoutes.detailCollectionFlashcard,
|
||||
arguments: data.collectionId,
|
||||
);
|
||||
await refreshData(refreshRemote: true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
import 'package:epic_story_app/feature/favorites/presentation/favorite_home/components/book_collection.dart';
|
||||
import 'package:epic_story_app/feature/favorites/presentation/favorite_home/components/flashcard_collection.dart';
|
||||
import 'package:epic_story_app/feature/favorites/presentation/favorite_home/favorite_home_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class FavoriteHomePage extends StatelessWidget {
|
||||
const FavoriteHomePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.find<FavoriteHomeController>();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
controller.goToCreateCollection();
|
||||
},
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
child: Image.asset(
|
||||
'assets/images/subway_add.png',
|
||||
width: 56,
|
||||
height: 56,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
body: Obx(() {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
height: 170,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFF7E32AB),
|
||||
Color(0xFFE0CEFF),
|
||||
],
|
||||
stops: [0.0, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 60,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: _TypeSwitcher(controller: controller),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 156),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: controller.onRefresh,
|
||||
child: controller.isBook.value
|
||||
? BookCollection(controller: controller)
|
||||
: const FlashcardCollection(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TypeSwitcher extends StatelessWidget {
|
||||
final FavoriteHomeController controller;
|
||||
|
||||
const _TypeSwitcher({required this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isBookActive = controller.isBook.value;
|
||||
|
||||
return Container(
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [Color(0xFFECB63F), Color(0xFF0088FF)],
|
||||
),
|
||||
border: Border.all(color: Colors.black87, width: 1.4),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x29000000),
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
AnimatedAlign(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOutCubic,
|
||||
alignment:
|
||||
isBookActive ? Alignment.centerLeft : Alignment.centerRight,
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 0.5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x33000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: Image.asset(
|
||||
_activeAssetPath(isBookActive),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _item(
|
||||
label: 'Buku',
|
||||
value: true,
|
||||
active: isBookActive,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _item(
|
||||
label: 'Flashcard',
|
||||
value: false,
|
||||
active: !isBookActive,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _activeAssetPath(bool isBookActive) {
|
||||
if (isBookActive) {
|
||||
return 'assets/images/button-book-active.png';
|
||||
}
|
||||
|
||||
return 'assets/images/button-flashcard-acive.png';
|
||||
}
|
||||
|
||||
Widget _item({
|
||||
required String label,
|
||||
required bool value,
|
||||
required bool active,
|
||||
}) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
onTap: () => controller.isBook.value = value,
|
||||
child: Center(
|
||||
child: AnimatedDefaultTextStyle(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOut,
|
||||
style: TextStyle(
|
||||
color: active ? Colors.transparent : Colors.white,
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w900,
|
||||
height: 1,
|
||||
shadows: active
|
||||
? null
|
||||
: const [
|
||||
Shadow(
|
||||
color: Color(0xFF171717),
|
||||
offset: Offset(1.2, 1.2),
|
||||
blurRadius: 0,
|
||||
),
|
||||
Shadow(
|
||||
color: Color(0xB3000000),
|
||||
offset: Offset(0, 0),
|
||||
blurRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Text(label),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue