// ignore_for_file: deprecated_member_use import 'package:didvan/config/theme_data.dart'; import 'package:didvan/models/content/content_item.dart'; import 'package:didvan/models/infography/infography_content.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/services/content/content_service.dart'; import 'package:didvan/utils/action_sheet.dart'; import 'package:didvan/utils/category_labels.dart'; import 'package:didvan/views/widgets/didvan/divider.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/floating_navigation_bar.dart'; import 'package:didvan/views/widgets/item_title.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:persian_number_utility/persian_number_utility.dart'; import 'package:url_launcher/url_launcher_string.dart'; /// شناسه‌ی دسته‌ی «بنر» (= اینفوگرافی) در content-service. const String _bannerCategoryId = '3e53627d-9a18-4bad-b4ae-1f0c55438147'; /// آیتم لیست «مطالب مرتبط» در صفحه‌ی detail. class _RelatedItem { final String uuid; final String title; final String image; final String metaCategory; final String createdAt; _RelatedItem({ required this.uuid, required this.title, required this.image, required this.metaCategory, required this.createdAt, }); } /// صفحه‌ی detail اینفوگرافی — استایل دقیقا مشابه news single page (DidvanPageView) /// با سه تفاوت درخواست‌شده: /// 1. لبه‌ی بالای کارت سفید (که روی تصویر می‌نشیند) **صاف** است (نه گرد). /// 2. FloatingNavigationBar شامل کامنت/منشن/بوکمارک (مشابه news). /// 3. بخش «مطالب مرتبط» با بنرهای هم‌دسته‌ی موضوعی. class InfographyDetails extends StatefulWidget { final Map pageData; const InfographyDetails({super.key, required this.pageData}); @override State createState() => _InfographyDetailsState(); } class _InfographyDetailsState extends State { final ScrollController _scrollController = ScrollController(); Content? _item; String? _description; bool _loading = false; bool _failed = false; // related contents List<_RelatedItem> _related = []; bool _relatedLoading = false; @override void initState() { super.initState(); _item = widget.pageData['item'] as Content?; final uuid = widget.pageData['contentUuid'] as String?; if (uuid != null && uuid.isNotEmpty) { _fetchDetail(uuid); } else if (_item != null) { _loadRelated(_item!.metaCategory, _item!.keycloakId); } } @override void dispose() { _scrollController.dispose(); super.dispose(); } Future _fetchDetail(String uuid) async { setState(() => _loading = true); try { final res = await ContentService.instance.getContent(uuid); if (res.isSuccess && res.data != null) { final c = res.data!; _item = Content.fromContentItem(c); _description = c.summary ?? ''; } else { _failed = true; } } catch (_) { _failed = true; } finally { if (mounted) setState(() => _loading = false); } if (_item != null) { _loadRelated(_item!.metaCategory, _item!.keycloakId); } } Future _loadRelated(String metaCategory, String? currentUuid) async { if (_relatedLoading) return; setState(() => _relatedLoading = true); try { // همه‌ی بنرها رو می‌گیریم بعد client-side با metaCategory فیلتر می‌کنیم. final res = await ContentService.instance.getContents( page: 1, limit: metaCategory.isEmpty ? 10 : 50, filterStatus: '\$in:accepted,published', categoryId: _bannerCategoryId, sortBy: const [MapEntry('createdAt', 'DESC')], ); if (res.isSuccess && res.data != null) { final all = res.data!.data; final filtered = []; for (final c in all) { if (c.id == currentUuid) continue; if (metaCategory.isNotEmpty && (c.metaCategory ?? '') != metaCategory) { continue; } filtered.add(c); if (filtered.length >= 8) break; } _related = filtered .map((c) => _RelatedItem( uuid: c.id, title: c.title, image: c.coverImage ?? '', metaCategory: c.metaCategory ?? '', createdAt: c.publishedAt?.toIso8601String() ?? '', )) .toList(); } } catch (_) { // silent } finally { if (mounted) setState(() => _relatedLoading = false); } } String get _categoryLabel { final m = _item?.metaCategory; if (m != null && m.isNotEmpty) { final t = CategoryLabels.label(m); if (t.isNotEmpty) return t; } return _item?.category ?? ''; } String _persianDate(String raw) { if (raw.isEmpty) return ''; try { return DateTime.parse(raw).toPersianDateStr(); } catch (_) { return ''; } } String _getCategoryIcon(String label) { if (label.contains('سیاسی')) return 'lib/assets/icons/SiasiNoBG.svg'; if (label.contains('کسب')) return 'lib/assets/icons/KasbokarNoBG.svg'; if (label.contains('اقتصادی')) return 'lib/assets/icons/Economic_NoBG.svg'; if (label.contains('اجتماعی')) return 'lib/assets/icons/EjtemaeiNoBg.svg'; if (label.contains('محیطی') || label.contains('زیست')) { return 'lib/assets/icons/ZistMohitNoBG.svg'; } if (label.contains('فناوری')) return 'lib/assets/icons/FanavariNoBG.svg'; return 'lib/assets/icons/element-2.svg'; } @override Widget build(BuildContext context) { final theme = Theme.of(context); if (_item == null && _loading) { return const Scaffold(body: Center(child: CircularProgressIndicator())); } if (_failed || _item == null) { return Scaffold( appBar: _appBar(context), body: const Center(child: Text('خطا در دریافت محتوا')), ); } final item = _item!; final dateStr = _persianDate(item.createdAt).toPersianDigit(); return Scaffold( backgroundColor: theme.colorScheme.background, appBar: _appBar(context), body: Stack( children: [ LayoutBuilder( builder: (context, constraints) { final double imageHeight = constraints.maxWidth / (16 / 9); const double overlap = 24.0; return SingleChildScrollView( controller: _scrollController, physics: const BouncingScrollPhysics(), padding: const EdgeInsets.only(bottom: 92), child: Stack( children: [ // === تصویر اصلی === Positioned( top: 0, left: 0, right: 0, height: imageHeight, child: GestureDetector( onTap: () => ActionSheetUtils(context) .openInteractiveViewer(context, item.image, false), child: SkeletonImage( imageUrl: item.image, borderRadius: BorderRadius.zero, width: double.infinity, ), ), ), // === کارت سفید روی تصویر، لبه‌ی بالای آن «صاف» === Container( margin: EdgeInsets.only(top: imageHeight - overlap), decoration: BoxDecoration( color: theme.colorScheme.background, // لبه صاف به‌جای BorderRadius.vertical(top: 24) borderRadius: BorderRadius.zero, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, -5), ), ], ), child: Padding( padding: const EdgeInsets.symmetric(vertical: 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // عنوان Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: DidvanText( item.title, style: theme.textTheme.titleMedium, ), ), const SizedBox(height: 8), // تاریخ if (dateStr.isNotEmpty) Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ SvgPicture.asset( 'lib/assets/icons/calendar.svg', color: theme.colorScheme.caption, height: 16, ), const SizedBox(width: 6), DidvanText( dateStr, style: theme.textTheme.bodySmall ?.copyWith( color: theme.colorScheme.caption), ), ], ), ), // === badge دسته‌بندی (زیر تاریخ، به‌جای روی عکس) === if (_categoryLabel.isNotEmpty) Padding( padding: const EdgeInsets.only( left: 16, right: 16, top: 8), child: Align( alignment: AlignmentDirectional.centerStart, child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6), decoration: BoxDecoration( color: const Color.fromARGB( 255, 230, 242, 246), borderRadius: BorderRadius.circular(16), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ SvgPicture.asset( _getCategoryIcon(_categoryLabel), height: 16, width: 16, color: const Color.fromARGB( 255, 25, 93, 128), ), const SizedBox(width: 6), DidvanText( _categoryLabel, style: theme.textTheme.bodySmall ?.copyWith( color: const Color.fromARGB( 255, 25, 93, 128), fontWeight: FontWeight.bold, fontSize: 12, ), ), ], ), ), ), ), // خلاصه/توضیحات if ((_description ?? '').isNotEmpty) ...[ const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ DidvanText( 'توضیحات:', style: theme.textTheme.bodyMedium ?.copyWith( color: theme.colorScheme.primary, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), DidvanText( _description!, style: theme.textTheme.bodyMedium ?.copyWith( color: const Color.fromARGB( 255, 102, 102, 102), ), ), const SizedBox(height: 2), const DidvanDivider(), ], ), ), ], // دکمه‌ی منبع if ((item.link ?? '').isNotEmpty) Padding( padding: const EdgeInsets.only( left: 16, right: 16, top: 16), child: SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: () => launchUrlString(item.link!), icon: const Icon(Icons.open_in_new, size: 18), label: const DidvanText('مشاهده منبع'), style: ElevatedButton.styleFrom( backgroundColor: theme.colorScheme.primary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ), // ====== مطالب مرتبط ====== const Padding( padding: EdgeInsets.only( left: 16, right: 16, top: 16, ), child: DidvanDivider(verticalPadding: 0), ), const Padding( padding: EdgeInsets.symmetric( horizontal: 16.0, vertical: 16.0), child: ItemTitle(title: 'مطالب مرتبط:'), ), if (_relatedLoading && _related.isEmpty) const Padding( padding: EdgeInsets.all(16), child: Center( child: CircularProgressIndicator()), ), if (!_relatedLoading && _related.isEmpty) Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8), child: DidvanText( 'مطلب مرتبطی یافت نشد.', style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.caption), ), ), for (final r in _related) Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 4), child: _RelatedCard(item: r), ), ], ), ), ), ], ), ); }, ), // === FloatingNavigationBar (مثل news single page) === Positioned( bottom: 0, left: 0, right: 0, child: FloatingNavigationBar( scrollController: _scrollController, item: _NavItem(item: item), openComments: widget.pageData['goToComment'] == true, hasUnmarkConfirmation: widget.pageData['hasUnmarkConfirmation'] ?? false, isRadar: false, onCommentsChanged: (count) { setState(() => item.comments = count); }, onMarkChanged: (value) { setState(() => _item = item..marked = value); final cb = widget.pageData['onMarkChanged']; if (cb is Function) { cb(item.id, value); } }, ), ), ], ), ); } PreferredSizeWidget _appBar(BuildContext context) { return PreferredSize( preferredSize: const Size.fromHeight(90.0), child: AppBar( backgroundColor: Theme.of(context).colorScheme.surface, elevation: 0, automaticallyImplyLeading: false, flexibleSpace: SafeArea( child: Container( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Center( child: SvgPicture.asset( 'lib/assets/images/logos/logo-horizontal-light.svg', height: 55, ), ), IconButton( icon: SvgPicture.asset( 'lib/assets/icons/arrow-left.svg', color: Theme.of(context).colorScheme.caption, height: 24, ), onPressed: () => Navigator.pop(context), ), ], ), ), ), ), ); } } /// Adapter که فیلدهای موردنیاز FloatingNavigationBar را از Content فراهم می‌کند. /// FloatingNavigationBar از dynamic استفاده می‌کند و فقط به فیلدهای زیر دسترسی /// می‌خواهد: `id`, `title`, `marked`, `comments`, `keycloakId`. class _NavItem { final Content item; _NavItem({required this.item}); int get id => item.id; String get title => item.title; bool get marked => item.marked; int get comments => item.comments; String? get keycloakId => item.keycloakId; } /// کارت کوچک «مطلب مرتبط». با تپ به همان صفحه‌ی detail با uuid جدید می‌رود. class _RelatedCard extends StatelessWidget { final _RelatedItem item; const _RelatedCard({required this.item}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return GestureDetector( onTap: () { Navigator.of(context).pushNamed( Routes.infographyDetails, arguments: { 'contentUuid': item.uuid, }, ); }, child: Container( height: 100, decoration: BoxDecoration( color: theme.colorScheme.surface, borderRadius: BorderRadius.circular(12), border: Border.all(color: theme.colorScheme.border), ), clipBehavior: Clip.hardEdge, child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox( width: 120, child: SkeletonImage( imageUrl: item.image, borderRadius: BorderRadius.zero, width: 120, height: 100, ), ), Expanded( child: Padding( padding: const EdgeInsets.all(10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ DidvanText( item.title, style: theme.textTheme.bodyMedium ?.copyWith(fontWeight: FontWeight.w600), maxLines: 2, overflow: TextOverflow.ellipsis, ), Row( children: [ if (item.metaCategory.isNotEmpty) ...[ SvgPicture.asset( 'lib/assets/icons/element-2.svg', color: theme.colorScheme.caption, height: 14, width: 14, ), const SizedBox(width: 4), DidvanText( CategoryLabels.label(item.metaCategory), style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.caption), ), ], ], ), ], ), ), ), ], ), ), ); } }