import 'package:collection/collection.dart'; import 'package:didvan/config/auth_config.dart'; import 'package:didvan/constants/assets.dart'; import 'package:didvan/models/category.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/infography/info_tag.dart'; import 'package:didvan/models/infography/infography_content.dart'; import 'package:didvan/models/requests/infography.dart'; import 'package:didvan/providers/core.dart'; import 'package:didvan/providers/user.dart'; import 'package:didvan/services/content/content_service.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; class InfographyScreenState extends CoreProvier { List contents = []; String search = ''; String lastSearch = ''; int lastPage = 1; int page = 1; final List selectedTags = []; List tags = []; final List selectedCats = []; List categories = []; bool isScrolled = false; bool shouldColapse = false; bool get filtering => selectedCats.length > 1 && selectedTags.length > 1; bool get searching => search.isNotEmpty; bool get isColapsed => (selectedCats.length == 1 && selectedTags.length == 1 && !filtering && isScrolled) || isScrolled; bool get isCategorySelected => selectedCats.length == 1 && selectedTags.length == 1 && !filtering; Future changeMark(int id, bool value) async { final item = contents.firstWhereOrNull((element) => element.id == id); // optimistic UI item?.marked = value; update(); // اگر آیتم از content-service جدید است (keycloakId/UUID دارد)، به API // جدید toggle bookmark می‌فرستیم. legacy فقط برای آیتم‌های قدیمی. final uuid = item?.keycloakId; if (uuid != null && uuid.isNotEmpty) { await ContentService.instance.toggleBookmark(uuid); } else { UserProvider.changeItemMark('infography', id, value); } } Future changeLiked(int id, bool value) async { final item = contents.firstWhereOrNull((element) => element.id == id); item?.liked = value; update(); final uuid = item?.keycloakId; if (uuid != null && uuid.isNotEmpty) { await ContentService.instance.toggleLike(uuid); } else { UserProvider.changeItemLiked('infography', id, value); } } /// شناسه‌ی دسته‌ی «بنر» در content-service (اینفوگرافی/بنر پایین). static const String _infographyCategoryId = '3e53627d-9a18-4bad-b4ae-1f0c55438147'; /// شناسه‌ی دسته‌ی «بنر فولادینفو» — تصویر کاور آخرین content این دسته /// به‌عنوان پوستر بالای صفحه‌ی فولادینفو در اپ نمایش داده می‌شود. static const String _foladinfoBannerCategoryId = 'd1a5e9b3-6f47-4c82-9e1d-3b8f2c5a4d76'; /// URL تصویر بنر فولادینفو که در بالای صفحه نمایش داده می‌شود؛ /// اگر null یا خالی بود، بنر اصلاً رندر نمی‌شود. String? topBannerImageUrl; /// فقط محتوای منتشرشده/تأییدشده. status فقط IN دارد. static const String _visibleStatusFilter = '\$in:accepted,published'; Future getInfographyContent({required int page}) async { this.page = page; lastSearch = search; if (page == 1) { contents.clear(); tags.clear(); appState = AppState.busy; } // اینفوگرافی از content-service جدید (دسته‌ی «بنر») خوانده می‌شود. // مسیر قدیمی فقط وقتی useLegacyApi روشن است اجرا می‌شود. if (!AuthConfig.useLegacyApi) { await _getInfographyFromContentService(page); return; } final service = RequestService(RequestHelper.infographyOverviews( args: InfographyRequestArgs( page: page, tag: selectedTags.map((e) => e.id).toList(), q: search == '' ? null : search, categories: selectedCats.map((e) => e.id).toList()))); final service2 = RequestService(RequestHelper.infographyTags()); await service.httpGet(); await service2.httpGet(); if (service.isSuccess && service2.isSuccess) { lastPage = service.result['lastPage']; final content = InfographyContent.fromJson(service.result); final content2 = InfoTagModel.fromJson(service2.result); contents.addAll(content.contents); tags.addAll(content2.tags); appState = AppState.idle; return; } appState = AppState.failed; } /// نگاشت label فارسی به کلیدِ canonical در metadata.category بنر. static const Map _categoryKeyByLabel = { 'اقتصادی': 'economic', 'سیاسی': 'political', 'فناوری': 'technology', 'کسب و کار': 'business', 'زیست‌محیطی': 'environmental', 'اجتماعی': 'social', }; /// دریافت اینفوگرافی از content-service (دسته‌ی «بنر») با pagination و search. /// در صورت انتخاب دسته‌ی موضوعی توسط کاربر (سیاسی، اقتصادی، ...) خروجی فقط /// مواردی را شامل می‌شود که `metadata.category` آن‌ها مطابق دسته‌ی انتخابی /// باشد. این فیلتر کلاینت‌ساید است چون content-service فعلا برای metadata /// filter ساخت‌یافته ندارد. Future _getInfographyFromContentService(int page) async { // اگر کاربر دسته‌ی موضوعی انتخاب کرده باشد، حد بالاتری گرفته می‌شود تا // پس از فیلتر کافی باشد. در غیر این صورت همان pagination معمول. final hasSelection = selectedCats.isNotEmpty; final res = await ContentService.instance.getContents( page: page, limit: hasSelection ? 60 : 15, search: search.isEmpty ? null : search, filterStatus: _visibleStatusFilter, categoryId: _infographyCategoryId, sortBy: const [MapEntry('createdAt', 'DESC')], ); if (!res.isSuccess || res.data == null) { appState = AppState.failed; return; } final data = res.data!; Iterable items = data.data.map(Content.fromContentItem); if (hasSelection) { final keys = selectedCats .map((c) => _categoryKeyByLabel[c.label]) .whereType() .toSet(); items = items.where((c) => keys.contains(c.metaCategory)); } if (page == 1) { contents.clear(); } contents.addAll(items); lastPage = data.totalPages; appState = AppState.idle; } /// آخرین content منتشرشده در دسته‌ی «بنر فولادینفو» را می‌گیریم و /// `coverImage` آن را در [topBannerImageUrl] می‌گذاریم. در صورت خطا /// یا خالی‌بودن، بنر نمایش داده نمی‌شود (مقدار null می‌ماند). Future _fetchTopBanner() async { try { final res = await ContentService.instance.getContents( page: 1, limit: 1, filterStatus: _visibleStatusFilter, categoryId: _foladinfoBannerCategoryId, sortBy: const [MapEntry('createdAt', 'DESC')], ); if (!res.isSuccess || res.data == null || res.data!.data.isEmpty) { topBannerImageUrl = null; update(); return; } final first = res.data!.data.first; // The list endpoint may already include coverImage; if not, fetch detail. String? cover = first.coverImage; if (cover == null || cover.isEmpty) { final detail = await ContentService.instance.getContent(first.id); if (detail.isSuccess && detail.data != null) { cover = detail.data!.coverImage; } } topBannerImageUrl = (cover != null && cover.isNotEmpty) ? cover : null; update(); } catch (_) { topBannerImageUrl = null; } } void resetFilters(bool isInit) { selectedCats.clear(); selectedTags.clear(); search = ''; lastSearch = ''; isScrolled = false; if (!isInit) { getInfographyContent(page: 1); } } void init() { // search = ''; // lastSearch = ''; resetFilters(true); Future.delayed(Duration.zero, () { getInfographyContent(page: 1); _fetchTopBanner(); }); categories = [ CategoryData( id: 1, label: 'اقتصادی', asset: Assets.economicCategoryIcon, ), CategoryData( id: 2, label: 'سیاسی', asset: Assets.politicalCategoryIcon, ), CategoryData( id: 3, label: 'فناوری', asset: Assets.techCategoryIcon, ), CategoryData( id: 4, label: 'کسب و کار', asset: Assets.businessCategoryIcon, ), CategoryData( id: 5, label: 'زیست‌محیطی', asset: Assets.enviromentalCategoryIcon, ), CategoryData( id: 6, label: 'اجتماعی', asset: Assets.socialCategoryIcon, ), ]; } }