// ignore_for_file: avoid_print import 'dart:async'; 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/overview_data.dart'; import 'package:didvan/providers/core.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/services/auth/jwt_utils.dart'; import 'package:didvan/services/auth/token_storage.dart'; import 'package:didvan/services/content/content_service.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; import 'package:flutter/material.dart'; class MenuItemType { final String label; final String asset; final String link; MenuItemType({ required this.label, required this.asset, required this.link, }); } class HomeState extends CoreProvier { int _currentPageIndex = 0; String search = ''; String lastSearch = ''; Timer? _debounce; String? startDate; String? endDate; int page = 1; int lastPage = 1; final List selectedCats = []; final List results = []; late TabController tabController; int unreadCount = 0; final FocusNode searchFieldFocusNode = FocusNode(); bool _canView(String type) { final token = TokenStorage.accessToken ?? ''; if (token.isEmpty) return true; return JwtUtils.canView(token, type); } String? _accessTypeForMenuItem(MenuItemType item) { switch (item.link) { case Routes.news: return 'news'; case Routes.radars: return 'radar'; case Routes.videocasts: return 'videocast'; case Routes.podcasts: return 'podcast'; case Routes.infography: return 'infography'; case Routes.aiSection: return 'hoshan'; } if (item.link.contains('trend.didvan')) return 'trend'; if (item.link.contains('tech.didvan')) return 'technology'; if (item.link.contains('risk.didvan')) return 'risk'; if (item.link.contains('startup.didvan')) return 'startup'; if (item.link.contains('opportunity-threat.didvan')) return 'swot'; return null; } List _filterMenuItemsByAccess(List items) { return items.where((item) { final accessType = _accessTypeForMenuItem(item); return accessType == null || _canView(accessType); }).toList(); } void onSearchChanged(String query) { if (_debounce?.isActive ?? false) _debounce?.cancel(); _debounce = Timer(const Duration(milliseconds: 1500), () { if (lastSearch != query && query.length >= 2) { search = query; searchAll(page: 1); } }); } void clearSearch() { if (_debounce?.isActive ?? false) _debounce?.cancel(); search = ''; lastSearch = ''; results.clear(); notifyListeners(); } void resetFilters(bool isInit) { startDate = null; endDate = null; selectedCats.clear(); search = ''; lastSearch = ''; if (!isInit) { searchAll(page: 1); } } set currentPageIndex(int value) { _currentPageIndex = value; notifyListeners(); } int get currentPageIndex => _currentPageIndex; List menuItems = []; List categories = []; bool get filtering => selectedCats.isNotEmpty || startDate != null || endDate != null || search.isNotEmpty; /// TODO(backend-migration): endpoint قدیمی `api.didvan.app/home/mark/v2` /// در دسترس نیست. پس از افزوده شدن endpoint مشابه در api2.didvan.com /// برای bookmark filtering، این متد باید به ContentService منتقل شود /// (مثلا یک پارامتر `bookmarkedBy=` در `GET /contents`). Future searchMarks({required int page}) async { this.page = page; if (page == 1) { results.clear(); appState = AppState.busy; } lastSearch = search; final service = RequestService( RequestHelper.searchMarks( page: page, search: search, types: selectedCats.map((e) => e.id).toList(), startDate: startDate, endDate: endDate, ), ); await service.httpGet(); if (service.isSuccess) { lastPage = service.result['lastPage']; results.addAll( List.from( service.result['contents'].map( (e) => OverviewData.fromJson(e), ), ), ); lastPage = service.result['lastPage']; appState = AppState.idle; return; } appState = AppState.failed; } Future searchAll({required int page}) async { this.page = page; if (page == 1) { results.clear(); appState = AppState.busy; } lastSearch = search; final isSwotSelected = selectedCats.any((element) => element.id == 8); final hasLegacyCategory = selectedCats .any((element) => const [1, 2, 3, 5, 7].contains(element.id)); if (isSwotSelected) { await _searchSwot(page); return; } if (hasLegacyCategory) { // TODO(backend-migration): این دسته‌بندی‌ها هنوز به api.didvan.app // متصلند که در دسترس نیست. پس از افزوده شدن endpoint مشابه در // api2.didvan.com، این شاخه را به ContentService منتقل کنید (مثلا // فیلتر بر اساس category یا tag). if (AuthConfig.useLegacyApi) { await _searchLegacy(page); } else { await _searchNewContents(page); } return; } await _searchNewContents(page); } Future _searchNewContents(int page) async { final result = await ContentService.instance.getContents( page: page, limit: 15, search: search.isEmpty ? null : search, filterStatus: '\$in:accepted,published', // publishedAt; sortBy: const [MapEntry('createdAt', 'DESC')], ); if (!result.isSuccess || result.data == null) { print('searchNewContents failed: ${result.errorMessage}'); appState = AppState.failed; return; } final pageData = result.data!; results.addAll(pageData.data.map(OverviewData.fromContentItem)); lastPage = pageData.totalPages; appState = AppState.idle; } static const String _swotCategoryId = 'f0092865-e004-4031-91c3-eca1fb0c6def'; static const String _swotVisibleStatus = '\$in:accepted,published'; Future _searchSwot(int page) async { try { final res = await ContentService.instance.getContents( page: page, limit: 15, search: search.isEmpty ? null : search, filterStatus: _swotVisibleStatus, categoryId: _swotCategoryId, sortBy: const [MapEntry('createdAt', 'DESC')], ); if (!res.isSuccess || res.data == null) { appState = AppState.failed; return; } final data = res.data!; results.addAll(data.data.map((c) => OverviewData( id: c.id.hashCode, keycloakId: c.id, title: c.title, image: c.coverImage ?? '', description: c.summary ?? '', createdAt: c.publishedAt?.toIso8601String() ?? '', type: 'swot', marked: c.isBookmarked, liked: c.isLiked, likes: c.likeCount, comments: 0, forManagers: false, link: OverviewData.webLinkForCategory(_swotCategoryId, c.id), ))); lastPage = data.totalPages; appState = AppState.idle; } catch (e) { print('Error fetching SWOT items: $e'); appState = AppState.failed; } } Future _searchLegacy(int page) async { final service = RequestService( RequestHelper.searchAll( page: page, search: search, endDate: endDate, types: selectedCats.map((e) => e.id).toList(), startDate: startDate, ), ); await service.httpGet(); if (service.isSuccess) { lastPage = service.result['lastPage']; unreadCount = service.result['unread'] ?? unreadCount; results.addAll( List.from( service.result['contents'].map( (e) => OverviewData.fromJson(e), ), ), ); lastPage = service.result['lastPage']; appState = AppState.idle; return; } appState = AppState.failed; } final categoryFilters = [ CategoryData(id: 1, label: 'پویش افق'), CategoryData(id: 2, label: 'دنیای فولاد'), CategoryData(id: 3, label: 'رسانه'), CategoryData(id: 5, label: 'رادارهای استراتژیک'), // CategoryData(id: 6, label: 'سها'), // حذف شده CategoryData(id: 8, label: 'بایدها و نبایدها'), // اضافه شده CategoryData(id: 7, label: 'ماهنامه تحلیلی'), ]; void refresh() async { menuItems = _filterMenuItemsByAccess([ MenuItemType( label: 'آمار و داده', asset: Assets.stats, link: 'tab-3', ), MenuItemType( label: 'دنیای فولاد', asset: Assets.fooladWorld, link: Routes.news, ), MenuItemType( label: 'پویش افق', asset: Assets.ofogh, link: Routes.radars, ), MenuItemType( label: 'ویدیوکست', asset: Assets.videocast, link: Routes.videocasts, ), MenuItemType( label: 'پادکست', asset: Assets.podcast, link: Routes.podcasts, ), MenuItemType( label: 'اینفوگرافی', asset: Assets.infography, link: Routes.infography, ), MenuItemType( label: 'رادار روند', asset: Assets.trend, link: 'https://trend.didvan.app/', ), MenuItemType( label: 'رادار تکنولوژی', asset: Assets.tech, link: 'https://tech.didvan.app/', ), MenuItemType( label: 'رادار ریسک', asset: Assets.risk, link: 'https://risk.didvan.app/', ), MenuItemType( label: 'رادار استارت‌آپ', asset: Assets.startup, link: 'https://startup.didvan.app/', ), MenuItemType( label: 'سها', asset: Assets.saha, // Points at the new "My Saha panels" landing on the panel origin. // The WebView already injects Keycloak auth cookies on .didvan.com, // so the page loads authenticated and only shows panels the user is // actually a member of. link: 'https://app.didvan.com/saha', ), MenuItemType( label: 'هوشان', asset: Assets.ai, link: Routes.aiSection, ), MenuItemType( label: 'فرصت و تهدید', asset: Assets.hugeideas, link: 'http://opportunity-threat.didvan.com/?accessToken=${RequestService.token}', ), ]); 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, ), 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, ), ]; Future.delayed(Duration.zero, notifyListeners); } @override void dispose() { _debounce?.cancel(); searchFieldFocusNode.dispose(); super.dispose(); } }