366 lines
12 KiB
Dart
366 lines
12 KiB
Dart
import 'dart:convert';
|
||
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/monthly/monthly_model.dart';
|
||
import 'package:didvan/providers/core.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';
|
||
import 'package:http/http.dart' as http;
|
||
|
||
class MonthlyListState extends CoreProvier {
|
||
List<MonthlyItem> monthlyItems = [];
|
||
List<MonthlyItem> _allMonthlyItems = [];
|
||
// برای content-service: id (hashCode) → uuid اصلی
|
||
final Map<int, String> _monthlyUuidById = {};
|
||
String searchQuery = '';
|
||
String? startDate;
|
||
String? endDate;
|
||
final List<CategoryData> selectedCats = [];
|
||
List<CategoryData> categories = [];
|
||
bool filtering = false;
|
||
|
||
void initCategories() {
|
||
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,
|
||
),
|
||
];
|
||
}
|
||
|
||
bool get searching => searchQuery.isNotEmpty;
|
||
|
||
/// شناسهی دستهی «ماهنامه» در content-service.
|
||
static const String _monthlyCategoryId =
|
||
'd5e8b2c4-7f31-4a92-bc6f-8e2a5d1c3b47';
|
||
|
||
/// تعداد صفحات را از body یا keywords محتوای content-service استخراج میکند.
|
||
int _extractPageCount(dynamic content) {
|
||
final body = content.body;
|
||
if (body is Map && body['blocks'] is List) {
|
||
for (final block in body['blocks'] as List) {
|
||
if (block is Map &&
|
||
(block['type'] == 'paragraph' || block['type'] == 'header') &&
|
||
block['data'] is Map) {
|
||
final text = (block['data']['text'] ?? '').toString().trim();
|
||
if (text.isNotEmpty) {
|
||
// ارقام فارسی به انگلیسی
|
||
final en = text.replaceAllMapped(RegExp('[۰-۹]'),
|
||
(m) => (m.group(0)!.codeUnitAt(0) - '۰'.codeUnitAt(0)).toString());
|
||
final digits = en.replaceAll(RegExp(r'[^0-9]'), '');
|
||
final n = int.tryParse(digits);
|
||
if (n != null) return n;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for (final k in content.keywords as List<String>) {
|
||
final en = k.replaceAllMapped(RegExp('[۰-۹]'),
|
||
(m) => (m.group(0)!.codeUnitAt(0) - '۰'.codeUnitAt(0)).toString());
|
||
final digits = en.replaceAll(RegExp(r'[^0-9]'), '');
|
||
final n = int.tryParse(digits);
|
||
if (n != null) return n;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
Future<void> fetchMonthlyList() async {
|
||
appState = AppState.busy;
|
||
notifyListeners();
|
||
|
||
// مسیر جدید: content-service.
|
||
if (!AuthConfig.useLegacyApi) {
|
||
try {
|
||
debugPrint('📅 monthly: fetching from content-service');
|
||
final res = await ContentService.instance.getContents(
|
||
page: 1,
|
||
limit: 30,
|
||
filterStatus: '\$in:accepted,published',
|
||
categoryId: _monthlyCategoryId,
|
||
sortBy: const [MapEntry('createdAt', 'DESC')],
|
||
);
|
||
debugPrint(
|
||
'📅 monthly: success=${res.isSuccess} items=${res.data?.data.length}');
|
||
if (!res.isSuccess || res.data == null) {
|
||
appState = AppState.failed;
|
||
notifyListeners();
|
||
return;
|
||
}
|
||
// endpoint لیست body نمیفرستد، پس برای هر آیتم detail کامل را
|
||
// میگیریم تا fileUrl پر شود.
|
||
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);
|
||
_monthlyUuidById.clear();
|
||
_allMonthlyItems = fullItems.map((c) {
|
||
final fileUrl = c.videoUrl ?? c.audioUrl ?? '';
|
||
final dateIso = (c.publishedAt ?? c.createdAt)?.toIso8601String() ??
|
||
DateTime.now().toIso8601String();
|
||
_monthlyUuidById[c.id.hashCode] = c.id;
|
||
return MonthlyItem(
|
||
id: c.id.hashCode,
|
||
title: c.title,
|
||
image: c.coverImage ?? '',
|
||
description: c.summary ?? '',
|
||
pageNumber: _extractPageCount(c),
|
||
publishedAt: dateIso,
|
||
file: fileUrl,
|
||
link: null,
|
||
tags: const <MonthlyTag>[],
|
||
isLiked: c.isLiked,
|
||
isBookmarked: c.isBookmarked,
|
||
);
|
||
}).toList();
|
||
monthlyItems = _applyFilters(_allMonthlyItems);
|
||
appState = AppState.idle;
|
||
} catch (e) {
|
||
debugPrint('❌ monthly (content-service) failed: $e');
|
||
appState = AppState.failed;
|
||
}
|
||
notifyListeners();
|
||
return;
|
||
}
|
||
|
||
// مسیر legacy.
|
||
try {
|
||
const url = '${RequestHelper.baseUrl}/monthly';
|
||
debugPrint('Fetching monthly list from: $url');
|
||
|
||
final response = await http.get(
|
||
Uri.parse(url),
|
||
headers: {
|
||
'Authorization': 'Bearer ${RequestService.token}',
|
||
'accept': '*/*',
|
||
'Content-Type': 'application/json; charset=UTF-8',
|
||
},
|
||
).timeout(const Duration(seconds: 30));
|
||
|
||
debugPrint('Response status code: ${response.statusCode}');
|
||
|
||
if (response.statusCode == 200) {
|
||
final jsonData = json.decode(response.body);
|
||
debugPrint('Decoded JSON: $jsonData');
|
||
|
||
final List<dynamic> resultList = jsonData['result'] as List;
|
||
debugPrint('Found ${resultList.length} items');
|
||
|
||
if (resultList.isNotEmpty) {
|
||
debugPrint('First item data: ${resultList.first}');
|
||
}
|
||
|
||
_allMonthlyItems =
|
||
resultList.map((item) => MonthlyItem.fromJson(item)).toList();
|
||
|
||
if (_allMonthlyItems.isNotEmpty) {
|
||
debugPrint('First item link: ${_allMonthlyItems.first.link}');
|
||
debugPrint('First item file: ${_allMonthlyItems.first.file}');
|
||
}
|
||
|
||
monthlyItems = _applyFilters(_allMonthlyItems);
|
||
|
||
debugPrint(
|
||
'Successfully parsed ${monthlyItems.length} monthly items after filters');
|
||
appState = AppState.idle;
|
||
} else {
|
||
debugPrint('Request failed with status code: ${response.statusCode}');
|
||
appState = AppState.failed;
|
||
}
|
||
} catch (e, stackTrace) {
|
||
debugPrint('Error fetching monthly list: $e');
|
||
debugPrint('Stack trace: $stackTrace');
|
||
appState = AppState.failed;
|
||
}
|
||
notifyListeners();
|
||
}
|
||
|
||
void search(String query) {
|
||
searchQuery = query;
|
||
var filtered = _allMonthlyItems;
|
||
|
||
if (query.isNotEmpty) {
|
||
filtered = filtered
|
||
.where((item) =>
|
||
item.title.toLowerCase().contains(query.toLowerCase()) ||
|
||
item.description.toLowerCase().contains(query.toLowerCase()))
|
||
.toList();
|
||
}
|
||
|
||
monthlyItems = _applyFilters(filtered);
|
||
notifyListeners();
|
||
}
|
||
|
||
Future<void> toggleLike(int id) async {
|
||
try {
|
||
final url = '${RequestHelper.baseUrl}/monthly/$id/like';
|
||
debugPrint('Toggling like for monthly item: $url');
|
||
|
||
final index = monthlyItems.indexWhere((item) => item.id == id);
|
||
if (index != -1) {
|
||
monthlyItems[index].isLiked = !monthlyItems[index].isLiked;
|
||
notifyListeners();
|
||
}
|
||
|
||
final response = await http.post(
|
||
Uri.parse(url),
|
||
headers: {
|
||
'Authorization': 'Bearer ${RequestService.token}',
|
||
'accept': '*/*',
|
||
'Content-Type': 'application/json; charset=UTF-8',
|
||
},
|
||
).timeout(const Duration(seconds: 30));
|
||
|
||
if (response.statusCode == 200) {
|
||
debugPrint('Like toggled successfully');
|
||
} else {
|
||
debugPrint('Failed to toggle like: ${response.statusCode}');
|
||
if (index != -1) {
|
||
monthlyItems[index].isLiked = !monthlyItems[index].isLiked;
|
||
notifyListeners();
|
||
}
|
||
}
|
||
} catch (e) {
|
||
debugPrint('Error toggling like: $e');
|
||
final index = monthlyItems.indexWhere((item) => item.id == id);
|
||
if (index != -1) {
|
||
monthlyItems[index].isLiked = !monthlyItems[index].isLiked;
|
||
notifyListeners();
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<void> toggleBookmark(int id) async {
|
||
final index = monthlyItems.indexWhere((item) => item.id == id);
|
||
if (index == -1) return;
|
||
|
||
// مسیر جدید: content-service.
|
||
if (!AuthConfig.useLegacyApi) {
|
||
// _allMonthlyItems را با _monthlyUuidById[id] نگه میداریم تا uuid را
|
||
// برای toggleBookmark بفرستیم.
|
||
final uuid = _monthlyUuidById[id];
|
||
if (uuid == null) return;
|
||
monthlyItems[index].isBookmarked = !monthlyItems[index].isBookmarked;
|
||
notifyListeners();
|
||
final res = await ContentService.instance.toggleBookmark(uuid);
|
||
if (!res.isSuccess) {
|
||
monthlyItems[index].isBookmarked = !monthlyItems[index].isBookmarked;
|
||
notifyListeners();
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
final url = '${RequestHelper.baseUrl}/monthly/$id/mark';
|
||
debugPrint('Toggling bookmark for monthly item: $url');
|
||
|
||
monthlyItems[index].isBookmarked = !monthlyItems[index].isBookmarked;
|
||
notifyListeners();
|
||
|
||
final response = await http.post(
|
||
Uri.parse(url),
|
||
headers: {
|
||
'Authorization': 'Bearer ${RequestService.token}',
|
||
'accept': '*/*',
|
||
'Content-Type': 'application/json; charset=UTF-8',
|
||
},
|
||
).timeout(const Duration(seconds: 30));
|
||
|
||
if (response.statusCode == 200) {
|
||
debugPrint('Bookmark toggled successfully');
|
||
} else {
|
||
debugPrint('Failed to toggle bookmark: ${response.statusCode}');
|
||
if (index != -1) {
|
||
monthlyItems[index].isBookmarked = !monthlyItems[index].isBookmarked;
|
||
notifyListeners();
|
||
}
|
||
}
|
||
} catch (e) {
|
||
debugPrint('Error toggling bookmark: $e');
|
||
final index = monthlyItems.indexWhere((item) => item.id == id);
|
||
if (index != -1) {
|
||
monthlyItems[index].isBookmarked = !monthlyItems[index].isBookmarked;
|
||
notifyListeners();
|
||
}
|
||
}
|
||
}
|
||
|
||
void resetFilters([bool notify = true]) {
|
||
startDate = null;
|
||
endDate = null;
|
||
selectedCats.clear();
|
||
filtering = false;
|
||
if (notify) {
|
||
fetchMonthlyList();
|
||
}
|
||
}
|
||
|
||
List<MonthlyItem> _applyFilters(List<MonthlyItem> items) {
|
||
// در مسیر جدید (content-service)، file از s3-service بهصورت async
|
||
// resolve میشود؛ پس صرفا بر اساس خالی بودن file آیتم را حذف نمیکنیم.
|
||
var filtered = AuthConfig.useLegacyApi
|
||
? items.where((item) => item.file.isNotEmpty).toList()
|
||
: items.toList();
|
||
|
||
if (startDate != null || endDate != null) {
|
||
filtered = filtered.where((item) {
|
||
final itemDate = DateTime.parse(item.publishedAt);
|
||
|
||
if (startDate != null) {
|
||
final start = DateTime.parse(startDate!);
|
||
if (itemDate.isBefore(start)) return false;
|
||
}
|
||
|
||
if (endDate != null) {
|
||
final end = DateTime.parse(endDate!).add(const Duration(days: 1));
|
||
if (itemDate.isAfter(end)) return false;
|
||
}
|
||
|
||
return true;
|
||
}).toList();
|
||
}
|
||
|
||
if (selectedCats.isNotEmpty) {
|
||
filtered = filtered.where((item) {
|
||
return item.tags
|
||
.any((tag) => selectedCats.any((cat) => cat.id == tag.id));
|
||
}).toList();
|
||
}
|
||
|
||
return filtered;
|
||
}
|
||
}
|