category repo
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../../domain/category/category.dart';
|
||||
|
||||
part 'category_loader_event.dart';
|
||||
part 'category_loader_state.dart';
|
||||
part 'category_loader_bloc.freezed.dart';
|
||||
|
||||
class CategoryLoaderBloc
|
||||
extends Bloc<CategoryLoaderEvent, CategoryLoaderState> {
|
||||
final ICategoryRepository _categoryRepository;
|
||||
|
||||
Timer? _searchDebounce;
|
||||
bool _isLoadingMore = false;
|
||||
|
||||
CategoryLoaderBloc(this._categoryRepository)
|
||||
: super(CategoryLoaderState.initial()) {
|
||||
on<CategoryLoaderEvent>(_onCategoryLoaderEvent);
|
||||
}
|
||||
|
||||
Future<void> _onCategoryLoaderEvent(
|
||||
CategoryLoaderEvent event,
|
||||
Emitter<CategoryLoaderState> emit,
|
||||
) {
|
||||
return event.map(
|
||||
getCategories: (e) async {
|
||||
emit(state.copyWith(isLoadingMore: true));
|
||||
|
||||
log(
|
||||
'📱 Loading categories - isActive: ${e.isActive}, forceRemote: ${e.forceRemote}',
|
||||
);
|
||||
|
||||
final result = await _categoryRepository.getCategories(
|
||||
page: 1,
|
||||
limit: 50,
|
||||
isActive: e.isActive,
|
||||
search: e.search,
|
||||
forceRemote: e.forceRemote,
|
||||
);
|
||||
|
||||
await result.fold(
|
||||
(failure) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoadingMore: false,
|
||||
failureOptionCategory: optionOf(failure),
|
||||
),
|
||||
);
|
||||
},
|
||||
(response) async {
|
||||
final categories = [Category.all(), ...response.categories];
|
||||
|
||||
final totalPages = response.totalPages;
|
||||
final hasReachedMax = categories.length < 50 || 1 >= totalPages;
|
||||
|
||||
log(
|
||||
'✅ Categories loaded: ${categories.length}, hasReachedMax: $hasReachedMax',
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
categories: categories,
|
||||
page: 1,
|
||||
hasReachedMax: hasReachedMax,
|
||||
isLoadingMore: false,
|
||||
failureOptionCategory: none(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
loadMore: (e) async {
|
||||
final currentState = state;
|
||||
|
||||
// ❌ HAPUS pengecekan is! _Loaded karena state cuma 1 class doang
|
||||
if (currentState.hasReachedMax ||
|
||||
_isLoadingMore ||
|
||||
currentState.isLoadingMore) {
|
||||
log(
|
||||
'⏹️ Load more blocked - hasReachedMax: ${currentState.hasReachedMax}, isLoadingMore: $_isLoadingMore',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_isLoadingMore = true;
|
||||
emit(currentState.copyWith(isLoadingMore: true));
|
||||
|
||||
final nextPage = currentState.page + 1; // ✅ Ganti currentPage jadi page
|
||||
log('📄 Loading more categories - page: $nextPage');
|
||||
|
||||
try {
|
||||
final result = await _categoryRepository.getCategories(
|
||||
page: nextPage,
|
||||
limit: 10,
|
||||
isActive: true,
|
||||
search: currentState.searchQuery,
|
||||
);
|
||||
|
||||
await result.fold(
|
||||
(failure) async {
|
||||
log('❌ Error loading more categories: $failure');
|
||||
emit(currentState.copyWith(isLoadingMore: false));
|
||||
},
|
||||
(response) async {
|
||||
final newCategories = response.categories;
|
||||
final totalPages = response.totalPages;
|
||||
|
||||
// Prevent duplicate categories
|
||||
final currentCategoryIds = currentState.categories
|
||||
.map((c) => c.id)
|
||||
.toSet();
|
||||
final filteredNewCategories = newCategories
|
||||
.where(
|
||||
(category) => !currentCategoryIds.contains(category.id),
|
||||
)
|
||||
.toList();
|
||||
|
||||
final allCategories = List<Category>.from(currentState.categories)
|
||||
..addAll(filteredNewCategories);
|
||||
|
||||
final hasReachedMax =
|
||||
newCategories.length < 10 || nextPage >= totalPages;
|
||||
|
||||
log(
|
||||
'✅ More categories loaded: ${filteredNewCategories.length} new, total: ${allCategories.length}',
|
||||
);
|
||||
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
categories: allCategories,
|
||||
hasReachedMax: hasReachedMax,
|
||||
page: nextPage, // ✅ Update page
|
||||
isLoadingMore: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
log('❌ Exception loading more categories: $e');
|
||||
emit(currentState.copyWith(isLoadingMore: false));
|
||||
} finally {
|
||||
_isLoadingMore = false;
|
||||
}
|
||||
},
|
||||
refresh: (e) async {
|
||||
final currentState = state;
|
||||
bool isActive = true;
|
||||
String? searchQuery = currentState.searchQuery;
|
||||
|
||||
_isLoadingMore = false;
|
||||
_searchDebounce?.cancel();
|
||||
|
||||
log('🔄 Refreshing categories');
|
||||
|
||||
// Clear local cache
|
||||
_categoryRepository.clearCache();
|
||||
|
||||
add(
|
||||
CategoryLoaderEvent.getCategories(
|
||||
isActive: isActive,
|
||||
search: searchQuery,
|
||||
forceRemote: true, // Force remote refresh
|
||||
),
|
||||
);
|
||||
},
|
||||
search: (e) async {
|
||||
// Cancel previous search
|
||||
_searchDebounce?.cancel();
|
||||
|
||||
// Debounce search for better UX
|
||||
_searchDebounce = Timer(Duration(milliseconds: 300), () async {
|
||||
emit(state.copyWith(isLoadingMore: true));
|
||||
_isLoadingMore = false;
|
||||
|
||||
log('🔍 Searching categories: "${e.query}"');
|
||||
|
||||
final result = await _categoryRepository.getCategories(
|
||||
page: 1,
|
||||
limit: 20, // More results for search
|
||||
isActive: e.isActive,
|
||||
search: e.query,
|
||||
);
|
||||
|
||||
await result.fold(
|
||||
(failure) async {
|
||||
log('❌ Search error: $failure');
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoadingMore: false,
|
||||
failureOptionCategory: optionOf(failure),
|
||||
),
|
||||
);
|
||||
},
|
||||
(response) async {
|
||||
final categories = [Category.all(), ...response.categories];
|
||||
final totalPages = response.totalPages;
|
||||
final hasReachedMax = categories.length < 20 || 1 >= totalPages;
|
||||
|
||||
log('✅ Search results: ${categories.length} categories found');
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
categories: categories,
|
||||
hasReachedMax: hasReachedMax,
|
||||
page: 1,
|
||||
isLoadingMore: false,
|
||||
failureOptionCategory: none(),
|
||||
searchQuery: e.query,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
syncAll: (e) async {
|
||||
emit(state.copyWith(isLoadingMore: true));
|
||||
|
||||
log('🔄 Starting full category sync...');
|
||||
|
||||
final result = await _categoryRepository.syncAllCategories();
|
||||
|
||||
await result.fold(
|
||||
(failure) async {
|
||||
log('❌ Sync failed: $failure');
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoadingMore: false,
|
||||
failureOptionCategory: optionOf(failure),
|
||||
),
|
||||
);
|
||||
|
||||
// After sync error, try to load local data
|
||||
Timer(Duration(seconds: 2), () {
|
||||
add(const CategoryLoaderEvent.getCategories());
|
||||
});
|
||||
},
|
||||
(successMessage) async {
|
||||
log('✅ Sync completed: $successMessage');
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoadingMore: false,
|
||||
failureOptionCategory: none(),
|
||||
),
|
||||
);
|
||||
|
||||
// After successful sync, load the updated data
|
||||
Timer(Duration(seconds: 1), () {
|
||||
add(const CategoryLoaderEvent.getCategories());
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
getAllCategories: (e) async {
|
||||
try {
|
||||
log('📋 Loading all categories for dropdown...');
|
||||
|
||||
// final categories = await _categoryRepository.getAllCategories();
|
||||
|
||||
// emit(
|
||||
// state.copyWith(
|
||||
// categories: categories,
|
||||
// isLoadingMore: false,
|
||||
// failureOptionCategory: none(),
|
||||
// ),
|
||||
// );
|
||||
// log('✅ All categories loaded: ${categories.length}');
|
||||
} catch (e) {
|
||||
log('❌ Error loading all categories: $e');
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoadingMore: false,
|
||||
failureOptionCategory: optionOf(
|
||||
CategoryFailure.dynamicErrorMessage(
|
||||
'Gagal memuat semua kategori: $e',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
getDatabaseStats: (e) async {
|
||||
try {
|
||||
final stats = await _categoryRepository.getDatabaseStats();
|
||||
log('📊 Category database stats retrieved: $stats');
|
||||
|
||||
// You can emit a special state here if needed for UI updates
|
||||
// For now, just log the stats
|
||||
} catch (e) {
|
||||
log('❌ Error getting category database stats: $e');
|
||||
}
|
||||
},
|
||||
clearCache: (e) async {
|
||||
log('🧹 Manually clearing category cache');
|
||||
_categoryRepository.clearCache();
|
||||
|
||||
// Refresh current data after cache clear
|
||||
add(const CategoryLoaderEvent.refresh());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_searchDebounce?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
part of 'category_loader_bloc.dart';
|
||||
|
||||
@freezed
|
||||
class CategoryLoaderEvent with _$CategoryLoaderEvent {
|
||||
const factory CategoryLoaderEvent.getCategories({
|
||||
@Default(true) bool isActive,
|
||||
String? search,
|
||||
@Default(false) bool forceRemote,
|
||||
}) = _GetCategories;
|
||||
|
||||
const factory CategoryLoaderEvent.loadMore() = _LoadMore;
|
||||
|
||||
const factory CategoryLoaderEvent.refresh() = _Refresh;
|
||||
|
||||
const factory CategoryLoaderEvent.search({
|
||||
required String query,
|
||||
@Default(true) bool isActive,
|
||||
}) = _Search;
|
||||
|
||||
const factory CategoryLoaderEvent.syncAll() = _SyncAll;
|
||||
|
||||
const factory CategoryLoaderEvent.getAllCategories() = _GetAllCategories;
|
||||
|
||||
const factory CategoryLoaderEvent.getDatabaseStats() = _GetDatabaseStats;
|
||||
|
||||
const factory CategoryLoaderEvent.clearCache() = _ClearCache;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
part of 'category_loader_bloc.dart';
|
||||
|
||||
@freezed
|
||||
class CategoryLoaderState with _$CategoryLoaderState {
|
||||
factory CategoryLoaderState({
|
||||
required List<Category> categories,
|
||||
required Option<CategoryFailure> failureOptionCategory,
|
||||
@Default(false) bool hasReachedMax,
|
||||
@Default(1) int page,
|
||||
@Default(false) bool isLoadingMore,
|
||||
String? searchQuery,
|
||||
}) = _CategoryLoaderState;
|
||||
|
||||
factory CategoryLoaderState.initial() =>
|
||||
CategoryLoaderState(categories: [], failureOptionCategory: none());
|
||||
}
|
||||
Reference in New Issue
Block a user