didvan-app/lib/views/monthly/monthly_list_state.dart

366 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}