783 lines
25 KiB
Dart
783 lines
25 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:didvan/config/auth_config.dart';
|
||
import 'package:didvan/utils/category_labels.dart';
|
||
import 'package:didvan/utils/country_labels.dart';
|
||
import 'package:didvan/main.dart';
|
||
import 'package:didvan/models/content/content_item.dart';
|
||
import 'package:didvan/models/didvan_plus_model.dart';
|
||
import 'package:didvan/models/didvan_voice_model.dart';
|
||
import 'package:didvan/models/enums.dart';
|
||
import 'package:didvan/models/home_page_content/home_page_content.dart';
|
||
import 'package:didvan/models/home_page_content/home_page_list.dart';
|
||
import 'package:didvan/models/home_page_content/content.dart';
|
||
import 'package:didvan/models/home_page_content/banner.dart';
|
||
import 'package:didvan/models/home_page_content/swot.dart';
|
||
import 'package:didvan/models/requests/infography.dart';
|
||
import 'package:didvan/models/requests/news.dart';
|
||
import 'package:didvan/models/requests/radar.dart';
|
||
import 'package:didvan/models/story_model.dart';
|
||
import 'package:didvan/models/top_banner_model.dart';
|
||
import 'package:didvan/providers/core.dart';
|
||
import 'package:didvan/routes/routes.dart';
|
||
import 'package:didvan/services/app_initalizer.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:didvan/services/story_service.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:url_launcher/url_launcher_string.dart';
|
||
|
||
const _legacyTimeout = Duration(seconds: 15);
|
||
|
||
class MainPageState extends CoreProvier {
|
||
MainPageContent? content;
|
||
int unread = 0;
|
||
List<UserStories> stories = [];
|
||
List<SwotItem> swotItems = [];
|
||
List<DidvanPlusModel> didvanPlusList = [];
|
||
DidvanPlusModel? didvanPlus;
|
||
DidvanVoiceModel? didvanVoice;
|
||
List<DidvanVoiceModel> didvanVoiceList = [];
|
||
TopBannerModel? topBanner;
|
||
|
||
List<ContentItem> contents = [];
|
||
|
||
List<MainPageList> exploreLists = [];
|
||
|
||
List<MainPageBannerType> fooladInfoBanners = [];
|
||
|
||
static const String _fooladInfoCategoryId =
|
||
'b84d4921-2ae5-4201-874c-48f646f8f57b';
|
||
|
||
static const List<({String type, String header, String categoryId})>
|
||
_exploreCategories = [
|
||
(
|
||
type: 'news',
|
||
header: 'دنیای فولاد',
|
||
categoryId: '442c2b9a-2002-43a9-8567-79900ed37534'
|
||
),
|
||
(
|
||
type: 'radar',
|
||
header: 'پویش افق',
|
||
categoryId: 'f97fbe3f-5712-4e27-81d4-b309cf58e91c'
|
||
),
|
||
(
|
||
type: 'trend',
|
||
header: 'رادار روند',
|
||
categoryId: 'ce2cdb6e-0268-4986-81a8-8a6e7bc06fdf'
|
||
),
|
||
(
|
||
type: 'technology',
|
||
header: 'رادار تکنولوژی',
|
||
categoryId: 'a026cf50-aead-47b9-bec5-1544e8bf1e92'
|
||
),
|
||
(
|
||
type: 'startup',
|
||
header: 'رادار استارتآپ',
|
||
categoryId: '29c27474-6bdb-4de0-8633-60cb8230faf4'
|
||
),
|
||
];
|
||
int _currentPage = 0;
|
||
int _totalPages = 1;
|
||
bool _isLoadingMore = false;
|
||
String? _searchQuery;
|
||
|
||
bool get hasMore => _currentPage < _totalPages;
|
||
bool get isLoadingMore => _isLoadingMore;
|
||
|
||
void init() {
|
||
debugPrint('🏠 MainPageState init called');
|
||
Future.delayed(Duration.zero, () async {
|
||
appState = AppState.busy;
|
||
contents = [];
|
||
_currentPage = 0;
|
||
_totalPages = 1;
|
||
try {
|
||
final tasks = <Future<void>>[
|
||
_loadNextPage().catchError((e) {
|
||
debugPrint('❌ Failed to load new contents page: $e');
|
||
}),
|
||
_loadExploreLists(),
|
||
_loadFooladInfoBanners(),
|
||
_fetchStories(),
|
||
_getSwotItems(),
|
||
_getDidvanPlus(),
|
||
_getDidvanVoice(),
|
||
];
|
||
if (AuthConfig.useLegacyApi) {
|
||
tasks.addAll([
|
||
_getMainPageContent(),
|
||
_getTopBanner(),
|
||
]);
|
||
} else {
|
||
debugPrint(
|
||
'ℹ️ Legacy api.didvan.app sections skipped (useLegacyApi=false)');
|
||
}
|
||
await Future.wait(tasks);
|
||
appState = AppState.idle;
|
||
} catch (e) {
|
||
debugPrint('❌ Main page init failed: $e');
|
||
appState = AppState.failed;
|
||
}
|
||
});
|
||
}
|
||
|
||
Future<void> loadMore() async {
|
||
if (_isLoadingMore || !hasMore) return;
|
||
_isLoadingMore = true;
|
||
notifyListeners();
|
||
try {
|
||
await _loadNextPage();
|
||
} catch (e) {
|
||
debugPrint('❌ Main page loadMore failed: $e');
|
||
} finally {
|
||
_isLoadingMore = false;
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
Future<void> search(String? query) async {
|
||
_searchQuery =
|
||
(query == null || query.trim().isEmpty) ? null : query.trim();
|
||
contents = [];
|
||
_currentPage = 0;
|
||
_totalPages = 1;
|
||
appState = AppState.busy;
|
||
try {
|
||
await _loadNextPage();
|
||
appState = AppState.idle;
|
||
} catch (e) {
|
||
debugPrint('❌ Main page search failed: $e');
|
||
appState = AppState.failed;
|
||
}
|
||
}
|
||
|
||
Future<void> _loadNextPage() async {
|
||
final result = await ContentService.instance.getContents(
|
||
page: _currentPage + 1,
|
||
limit: 20,
|
||
search: _searchQuery,
|
||
filterStatus: _visibleStatusFilter,
|
||
sortBy: const [MapEntry('createdAt', 'DESC')],
|
||
);
|
||
|
||
if (!result.isSuccess || result.data == null) {
|
||
throw Exception(result.errorMessage ?? 'خطا در دریافت محتوا');
|
||
}
|
||
|
||
final page = result.data!;
|
||
contents.addAll(page.data);
|
||
_currentPage = page.currentPage;
|
||
_totalPages = page.totalPages;
|
||
notifyListeners();
|
||
}
|
||
|
||
static const String _visibleStatusFilter = '\$in:accepted,published';
|
||
|
||
Future<void> _loadExploreLists() async {
|
||
final lists = <MainPageList>[];
|
||
for (final cat in _exploreCategories) {
|
||
try {
|
||
final res = await ContentService.instance.getContents(
|
||
page: 1,
|
||
limit: 10,
|
||
filterStatus: _visibleStatusFilter,
|
||
categoryId: cat.categoryId,
|
||
sortBy: const [MapEntry('createdAt', 'DESC')],
|
||
);
|
||
final items =
|
||
(res.isSuccess ? res.data?.data : null) ?? const <ContentItem>[];
|
||
if (items.isEmpty) continue;
|
||
lists.add(MainPageList(
|
||
type: cat.type,
|
||
header: cat.header,
|
||
more: 'مشاهده همه',
|
||
link: _seeAllLinkFor(cat.type),
|
||
contents: items.map((c) => _contentToMainItem(c, cat.type)).toList(),
|
||
));
|
||
} catch (e) {
|
||
debugPrint('❌ explore list (${cat.type}) failed: $e');
|
||
}
|
||
}
|
||
exploreLists = lists;
|
||
notifyListeners();
|
||
}
|
||
|
||
Future<void> _loadFooladInfoBanners() async {
|
||
try {
|
||
final res = await ContentService.instance.getContents(
|
||
page: 1,
|
||
limit: 15,
|
||
filterStatus: _visibleStatusFilter,
|
||
categoryId: _fooladInfoCategoryId,
|
||
sortBy: const [MapEntry('createdAt', 'DESC')],
|
||
);
|
||
final items =
|
||
(res.isSuccess ? res.data?.data : null) ?? const <ContentItem>[];
|
||
fooladInfoBanners = items
|
||
.where((c) => (c.coverImage ?? '').isNotEmpty)
|
||
.map((c) => MainPageBannerType(image: c.coverImage!, link: null))
|
||
.toList();
|
||
notifyListeners();
|
||
} catch (e) {
|
||
debugPrint('❌ fooladInfo banners failed: $e');
|
||
}
|
||
}
|
||
|
||
String _webLinkFor(String type, ContentItem c) {
|
||
String? slug;
|
||
switch (type) {
|
||
case 'trend':
|
||
slug = 'trend';
|
||
break;
|
||
case 'technology':
|
||
slug = 'technology';
|
||
break;
|
||
case 'startup':
|
||
slug = 'startup';
|
||
break;
|
||
}
|
||
if (slug == null) return '';
|
||
return 'https://app.didvan.com/$slug/${c.id}';
|
||
}
|
||
|
||
String _seeAllLinkFor(String type) {
|
||
switch (type) {
|
||
case 'news':
|
||
return Routes.news;
|
||
case 'radar':
|
||
return Routes.radars;
|
||
case 'trend':
|
||
return 'https://app.didvan.com/trend';
|
||
case 'technology':
|
||
return 'https://app.didvan.com/technology';
|
||
case 'startup':
|
||
return 'https://app.didvan.com/startup';
|
||
default:
|
||
return '';
|
||
}
|
||
}
|
||
|
||
MainPageContentType _contentToMainItem(ContentItem c, String type) {
|
||
final dateIso =
|
||
c.publishedAt?.toIso8601String() ?? DateTime.now().toIso8601String();
|
||
List<String> subtitles;
|
||
switch (type) {
|
||
case 'news':
|
||
case 'radar':
|
||
subtitles = [dateIso];
|
||
break;
|
||
case 'startup':
|
||
// subtitles[0] = ؛
|
||
// subtitles[1] = خلاصه.
|
||
subtitles = [
|
||
CountryLabels.label(c.metaHeadquarter),
|
||
c.summary ?? '',
|
||
];
|
||
break;
|
||
case 'trend':
|
||
final translated = CategoryLabels.label(c.metaCategory);
|
||
subtitles = [translated.isNotEmpty ? translated : (c.summary ?? '')];
|
||
break;
|
||
case 'technology':
|
||
final translated = CategoryLabels.label(c.metaCategory);
|
||
subtitles = [translated.isNotEmpty ? translated : (c.summary ?? '')];
|
||
break;
|
||
default:
|
||
subtitles = [c.summary ?? ''];
|
||
}
|
||
return MainPageContentType(
|
||
id: c.id.hashCode,
|
||
contentId: c.id,
|
||
title: c.title,
|
||
image: (c.coverImage != null && c.coverImage!.isNotEmpty)
|
||
? c.coverImage!
|
||
: '',
|
||
link: _webLinkFor(type, c),
|
||
marked: c.isBookmarked,
|
||
isViewed: c.isRead,
|
||
subtitles: subtitles,
|
||
description: c.summary,
|
||
);
|
||
}
|
||
|
||
Future<void> toggleBookmark(ContentItem item) async {
|
||
final originalState = item.isBookmarked;
|
||
_updateContentItem(item.copyWith(isBookmarked: !originalState));
|
||
final result = await ContentService.instance.toggleBookmark(item.id);
|
||
if (!result.isSuccess) {
|
||
_updateContentItem(item.copyWith(isBookmarked: originalState));
|
||
}
|
||
}
|
||
|
||
Future<void> toggleLike(ContentItem item) async {
|
||
final originalLiked = item.isLiked;
|
||
final originalCount = item.likeCount;
|
||
_updateContentItem(item.copyWith(
|
||
isLiked: !originalLiked,
|
||
likeCount: originalLiked ? originalCount - 1 : originalCount + 1,
|
||
));
|
||
final result = await ContentService.instance.toggleLike(item.id);
|
||
if (!result.isSuccess) {
|
||
_updateContentItem(
|
||
item.copyWith(isLiked: originalLiked, likeCount: originalCount));
|
||
}
|
||
}
|
||
|
||
void _updateContentItem(ContentItem updated) {
|
||
final idx = contents.indexWhere((c) => c.id == updated.id);
|
||
if (idx == -1) return;
|
||
contents[idx] = updated;
|
||
notifyListeners();
|
||
}
|
||
|
||
Future<void> _getMainPageContent() async {
|
||
try {
|
||
final service = RequestService(RequestHelper.mainPageContent);
|
||
await service.httpGet().timeout(_legacyTimeout);
|
||
if (service.isSuccess) {
|
||
content = MainPageContent.fromJson(service.result);
|
||
unread = (service.result['unread'] as int?) ?? 0;
|
||
notifyListeners();
|
||
} else {
|
||
debugPrint('⚠️ mainPageContent legacy: status=${service.statusCode}');
|
||
}
|
||
} on TimeoutException {
|
||
debugPrint('⏱️ mainPageContent legacy timeout');
|
||
} catch (e) {
|
||
debugPrint('❌ mainPageContent legacy failed: $e');
|
||
}
|
||
}
|
||
|
||
static const String _storyCategoryId = '114366e1-48d0-43ad-9184-c2c67a7dded8';
|
||
|
||
Future<void> _fetchStories() async {
|
||
if (!AuthConfig.useLegacyApi) {
|
||
await _fetchStoriesFromContentService();
|
||
return;
|
||
}
|
||
try {
|
||
stories = await StoryService.getStories().timeout(_legacyTimeout);
|
||
notifyListeners();
|
||
} on TimeoutException {
|
||
debugPrint('⏱️ stories legacy timeout');
|
||
stories = [];
|
||
} catch (e) {
|
||
debugPrint('❌ stories legacy failed: $e');
|
||
stories = [];
|
||
}
|
||
}
|
||
|
||
Future<void> _fetchStoriesFromContentService() async {
|
||
try {
|
||
final res = await ContentService.instance.getContents(
|
||
page: 1,
|
||
limit: 50,
|
||
filterStatus: _visibleStatusFilter,
|
||
categoryId: _storyCategoryId,
|
||
sortBy: const [MapEntry('createdAt', 'DESC')],
|
||
);
|
||
if (!res.isSuccess || res.data == null) {
|
||
stories = [];
|
||
return;
|
||
}
|
||
final items = res.data!.data;
|
||
|
||
final Map<String, List<ContentItem>> grouped = {};
|
||
for (final c in items) {
|
||
final key = (c.metaStoryType ?? 'other');
|
||
grouped.putIfAbsent(key, () => <ContentItem>[]).add(c);
|
||
}
|
||
|
||
final list = <UserStories>[];
|
||
var groupId = 1;
|
||
grouped.forEach((typeKey, groupItems) {
|
||
final storyItems = <StoryItem>[];
|
||
var itemId = 1;
|
||
for (final c in groupItems) {
|
||
final url = (c.videoUrl != null && c.videoUrl!.isNotEmpty)
|
||
? c.videoUrl!
|
||
: (c.coverImage ?? '');
|
||
if (url.isEmpty) continue;
|
||
storyItems.add(StoryItem(
|
||
id: itemId++,
|
||
url: url,
|
||
media: (c.videoUrl != null && c.videoUrl!.isNotEmpty)
|
||
? MediaType.video
|
||
: MediaType.image,
|
||
duration: const Duration(seconds: 10),
|
||
viewed: c.isRead,
|
||
contentUuid: c.id,
|
||
));
|
||
}
|
||
if (storyItems.isEmpty) return;
|
||
list.add(UserStories(
|
||
id: groupId++,
|
||
user: User(
|
||
name: _persianStoryType(typeKey),
|
||
profileImageUrl: _iconForStoryType(typeKey),
|
||
createdAt: DateTime.now().toIso8601String(),
|
||
),
|
||
stories: storyItems,
|
||
));
|
||
});
|
||
|
||
stories = list;
|
||
notifyListeners();
|
||
} catch (e) {
|
||
debugPrint('❌ stories (content-service) failed: $e');
|
||
stories = [];
|
||
}
|
||
}
|
||
|
||
String _iconForStoryType(String type) {
|
||
switch (type) {
|
||
case 'macro-trends':
|
||
return 'lib/assets/icons/MACRO_TRENDS_N.png';
|
||
case 'macro-environment':
|
||
return 'lib/assets/icons/MACRO_ENVIRONMENT_N.png';
|
||
case 'industry-environment':
|
||
return 'lib/assets/icons/INDUSTRY_ENVIRONMENT_N.png';
|
||
case 'definition':
|
||
return 'lib/assets/icons/CONCEPTS_N.png';
|
||
case 'investment':
|
||
return 'lib/assets/icons/MODERN_INVESTMENTS_N.png';
|
||
default:
|
||
return 'lib/assets/icons/CONCEPTS_N.png';
|
||
}
|
||
}
|
||
|
||
String _persianStoryType(String type) {
|
||
switch (type) {
|
||
case 'macro-trends':
|
||
return 'روندهای کلان';
|
||
case 'macro-environment':
|
||
return 'محیط کلان';
|
||
case 'industry-environment':
|
||
return 'محیط صنعت';
|
||
case 'definition':
|
||
return 'مفاهیم';
|
||
case 'investment':
|
||
return 'سرمایهگذاری';
|
||
default:
|
||
return 'سایر';
|
||
}
|
||
}
|
||
|
||
static const String _swotCategoryId = 'f0092865-e004-4031-91c3-eca1fb0c6def';
|
||
|
||
Future<void> _getSwotItems() async {
|
||
try {
|
||
final res = await ContentService.instance.getContents(
|
||
page: 1,
|
||
limit: 10,
|
||
filterStatus: _visibleStatusFilter,
|
||
categoryId: _swotCategoryId,
|
||
sortBy: const [MapEntry('createdAt', 'DESC')],
|
||
);
|
||
final items =
|
||
(res.isSuccess ? res.data?.data : null) ?? const <ContentItem>[];
|
||
swotItems = items.map(SwotItem.fromContentItem).toList();
|
||
notifyListeners();
|
||
} catch (e) {
|
||
debugPrint('❌ swot (content-service) failed: $e');
|
||
}
|
||
}
|
||
|
||
static const String _didvanPlusCategoryId =
|
||
'e4c8a3b1-9d27-4e6f-bf3d-2a1c8e5f4d12';
|
||
|
||
Future<void> _getDidvanPlus() async {
|
||
if (!AuthConfig.useLegacyApi) {
|
||
try {
|
||
final res = await ContentService.instance.getContents(
|
||
page: 1,
|
||
limit: 20,
|
||
filterStatus: _visibleStatusFilter,
|
||
categoryId: _didvanPlusCategoryId,
|
||
sortBy: const [MapEntry('createdAt', 'DESC')],
|
||
);
|
||
if (!res.isSuccess || res.data == null) return;
|
||
final fetches = res.data!.data.map((c) async {
|
||
final detail = await ContentService.instance.getContent(c.id);
|
||
if (detail.isSuccess && detail.data != null) {
|
||
return detail.data!;
|
||
}
|
||
return c;
|
||
}).toList();
|
||
final fullItems = await Future.wait(fetches);
|
||
didvanPlusList = fullItems
|
||
.map((c) => DidvanPlusModel(
|
||
id: c.id.hashCode,
|
||
title: c.title,
|
||
description: c.summary ?? '',
|
||
image: c.coverImage ?? '',
|
||
file: c.videoUrl ?? c.audioUrl ?? '',
|
||
publishedAt: (c.publishedAt ?? c.createdAt)
|
||
?.toIso8601String() ??
|
||
DateTime.now().toIso8601String(),
|
||
duration: c.mediaDuration,
|
||
))
|
||
.toList();
|
||
if (didvanPlusList.isNotEmpty) {
|
||
didvanPlus = didvanPlusList.first;
|
||
}
|
||
notifyListeners();
|
||
} catch (e) {
|
||
debugPrint('❌ didvanPlus (content-service) failed: $e');
|
||
}
|
||
return;
|
||
}
|
||
|
||
// مسیر legacy.
|
||
try {
|
||
final service = RequestService(RequestHelper.didvanPlus);
|
||
await service.httpGet().timeout(_legacyTimeout);
|
||
if (service.statusCode == 200) {
|
||
final rawData = service.data('result');
|
||
if (rawData is List && rawData.isNotEmpty) {
|
||
didvanPlusList = rawData
|
||
.map((item) =>
|
||
DidvanPlusModel.fromJson(Map<String, dynamic>.from(item)))
|
||
.toList();
|
||
didvanPlus = didvanPlusList.first;
|
||
notifyListeners();
|
||
} else if (rawData is Map) {
|
||
didvanPlus =
|
||
DidvanPlusModel.fromJson(Map<String, dynamic>.from(rawData));
|
||
didvanPlusList = [didvanPlus!];
|
||
notifyListeners();
|
||
}
|
||
}
|
||
} on TimeoutException {
|
||
debugPrint('⏱️ didvanPlus legacy timeout');
|
||
} catch (e) {
|
||
debugPrint('❌ didvanPlus legacy failed: $e');
|
||
}
|
||
}
|
||
|
||
static const String _didvanVoiceCategoryId =
|
||
'a7f3c9e2-5b18-4d6a-9c7e-3f8b1d5e2a91';
|
||
|
||
Future<void> _getDidvanVoice() async {
|
||
if (!AuthConfig.useLegacyApi) {
|
||
try {
|
||
final res = await ContentService.instance.getContents(
|
||
page: 1,
|
||
limit: 20,
|
||
filterStatus: _visibleStatusFilter,
|
||
categoryId: _didvanVoiceCategoryId,
|
||
sortBy: const [MapEntry('createdAt', 'DESC')],
|
||
);
|
||
if (!res.isSuccess || res.data == null) return;
|
||
// endpoint لیست body نمیفرستد، پس برای هر آیتم detail کامل را
|
||
// میگیریم تا audioUrl پر شود.
|
||
final fetches = res.data!.data.map((c) async {
|
||
final detail = await ContentService.instance.getContent(c.id);
|
||
if (detail.isSuccess && detail.data != null) {
|
||
return detail.data!;
|
||
}
|
||
return c;
|
||
}).toList();
|
||
final fullItems = await Future.wait(fetches);
|
||
didvanVoiceList = fullItems
|
||
.map((c) => DidvanVoiceModel(
|
||
id: c.id.hashCode,
|
||
title: c.title,
|
||
file: c.audioUrl ?? '',
|
||
image: c.coverImage ?? '',
|
||
publishedAt: (c.publishedAt ?? c.createdAt)
|
||
?.toIso8601String() ??
|
||
DateTime.now().toIso8601String(),
|
||
))
|
||
.toList();
|
||
if (didvanVoiceList.isNotEmpty) {
|
||
didvanVoice = _pickLatestVoice(List.from(didvanVoiceList));
|
||
}
|
||
notifyListeners();
|
||
} catch (e) {
|
||
debugPrint('❌ didvanVoice (content-service) failed: $e');
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
final service = RequestService(RequestHelper.didvanVoice);
|
||
await service.httpGet().timeout(_legacyTimeout);
|
||
if (service.statusCode == 200) {
|
||
final rawData = service.data('result');
|
||
if (rawData is List && rawData.isNotEmpty) {
|
||
didvanVoiceList = rawData
|
||
.map((e) => DidvanVoiceModel.fromJson(
|
||
Map<String, dynamic>.from(e as Map)))
|
||
.toList();
|
||
didvanVoice = _pickLatestVoice(List.from(didvanVoiceList));
|
||
notifyListeners();
|
||
} else if (rawData is Map) {
|
||
didvanVoice =
|
||
DidvanVoiceModel.fromJson(Map<String, dynamic>.from(rawData));
|
||
didvanVoiceList = [didvanVoice!];
|
||
notifyListeners();
|
||
}
|
||
}
|
||
} on TimeoutException {
|
||
debugPrint('⏱️ didvanVoice legacy timeout');
|
||
} catch (e) {
|
||
debugPrint('❌ didvanVoice legacy failed: $e');
|
||
}
|
||
}
|
||
|
||
// TODO(backend-migration): `api.didvan.app/topBanner` فعلا در دسترس نیست.
|
||
// پس از پیادهسازی endpoint معادل در api2.didvan.com، این تابع را به آن
|
||
// متصل کنید.
|
||
Future<void> _getTopBanner() async {
|
||
try {
|
||
final service = RequestService(RequestHelper.topBanner);
|
||
await service.httpGet().timeout(_legacyTimeout);
|
||
if (service.statusCode == 200) {
|
||
final data = service.result['result'] ?? service.result;
|
||
topBanner = TopBannerModel.fromJson(data);
|
||
notifyListeners();
|
||
}
|
||
} on TimeoutException {
|
||
debugPrint('⏱️ topBanner legacy timeout');
|
||
} catch (e) {
|
||
debugPrint('❌ topBanner legacy failed: $e');
|
||
}
|
||
}
|
||
|
||
DidvanVoiceModel? _pickLatestVoice(List<DidvanVoiceModel> items) {
|
||
if (items.isEmpty) return null;
|
||
items.sort((a, b) {
|
||
try {
|
||
return DateTime.parse(b.publishedAt)
|
||
.compareTo(DateTime.parse(a.publishedAt));
|
||
} catch (_) {
|
||
return b.id.compareTo(a.id);
|
||
}
|
||
});
|
||
return items.first;
|
||
}
|
||
|
||
void markChangeHandler(String type, int id, bool value) {
|
||
content?.lists
|
||
.firstWhere((element) => element.type == type)
|
||
.contents
|
||
.firstWhere((element) => element.id == id)
|
||
.marked = value;
|
||
notifyListeners();
|
||
}
|
||
|
||
Future<void> _fetchMonthlyDetailsAndNavigate(int id, String? title) async {
|
||
final context = navigatorKey.currentContext!;
|
||
debugPrint('🔄 Fetching monthly details for ID: $id');
|
||
try {
|
||
final url = '${RequestHelper.baseUrl}/monthly/$id';
|
||
final service = RequestService(url);
|
||
await service.httpGet().timeout(_legacyTimeout);
|
||
if (service.isSuccess) {
|
||
final data = service.result.containsKey('result')
|
||
? service.result['result']
|
||
: service.result;
|
||
final fetchedFile = data['file'];
|
||
final fetchedLink = data['link'];
|
||
if (fetchedLink != null && fetchedLink.toString().isNotEmpty) {
|
||
Navigator.of(context).pushNamed(
|
||
Routes.web,
|
||
arguments: fetchedLink.toString(),
|
||
);
|
||
return;
|
||
}
|
||
if (fetchedFile != null && fetchedFile.toString().isNotEmpty) {
|
||
Navigator.of(context).pushNamed(
|
||
Routes.pdfViewer,
|
||
arguments: {
|
||
'pdfUrl': fetchedFile.toString(),
|
||
'title': title ?? '',
|
||
},
|
||
);
|
||
return;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
debugPrint('❌ Error fetching monthly details: $e');
|
||
}
|
||
debugPrint('⚠️ No file/link found, going to list');
|
||
Navigator.of(context).pushNamed(Routes.monthlyList);
|
||
}
|
||
|
||
void navigationHandler(
|
||
String type,
|
||
dynamic id,
|
||
String? link, {
|
||
String? description,
|
||
String? file,
|
||
}) {
|
||
link = link ?? '';
|
||
final context = navigatorKey.currentContext;
|
||
if (context == null) return;
|
||
|
||
dynamic args;
|
||
switch (type) {
|
||
case 'infography':
|
||
Navigator.of(context).pushNamed(Routes.infography, arguments: {
|
||
'onMarkChanged': (id, value) => markChangeHandler(type, id, value),
|
||
'id': id is int ? id : 0,
|
||
'args': const InfographyRequestArgs(page: 0),
|
||
'hasUnmarkConfirmation': false,
|
||
});
|
||
return;
|
||
case 'news':
|
||
Navigator.of(context).pushNamed(Routes.newsDetails, arguments: {
|
||
'onMarkChanged': (id, value) => markChangeHandler(type, id, value),
|
||
'id': id is int ? id : 0,
|
||
'args': const NewsRequestArgs(page: 0),
|
||
'hasUnmarkConfirmation': false,
|
||
'description': description,
|
||
});
|
||
return;
|
||
case 'radar':
|
||
Navigator.of(context).pushNamed(Routes.radarDetails, arguments: {
|
||
'onMarkChanged': (id, value) => markChangeHandler(type, id, value),
|
||
'id': id is int ? id : 0,
|
||
'args': const RadarRequestArgs(page: 0),
|
||
'hasUnmarkConfirmation': false,
|
||
'description': description,
|
||
});
|
||
return;
|
||
case 'video':
|
||
args = {'type': 'podcast', 'id': id};
|
||
Navigator.of(context).pushNamed(Routes.studioDetails, arguments: args);
|
||
return;
|
||
case 'monthly':
|
||
if (link.isNotEmpty) {
|
||
Navigator.of(context).pushNamed(Routes.web, arguments: link);
|
||
return;
|
||
}
|
||
if (file != null && file.isNotEmpty && file != 'null') {
|
||
Navigator.of(context).pushNamed(Routes.pdfViewer, arguments: {
|
||
'pdfUrl': file,
|
||
'title': description ?? '',
|
||
});
|
||
return;
|
||
}
|
||
if (id is int) {
|
||
_fetchMonthlyDetailsAndNavigate(id, description);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (link.isEmpty) return;
|
||
if (link.startsWith('http')) {
|
||
AppInitializer.openWebLink(
|
||
context,
|
||
'$link?accessToken=${RequestService.token}',
|
||
mode: LaunchMode.inAppWebView,
|
||
);
|
||
return;
|
||
}
|
||
Navigator.of(context).pushNamed(link, arguments: args);
|
||
}
|
||
}
|