243 lines
9.2 KiB
Dart
243 lines
9.2 KiB
Dart
// ignore_for_file: deprecated_member_use
|
|
|
|
import 'package:didvan/config/design_config.dart';
|
|
import 'package:didvan/config/theme_data.dart';
|
|
import 'package:didvan/models/infography/infography_content.dart' as inf;
|
|
import 'package:didvan/models/tag.dart';
|
|
import 'package:didvan/routes/routes.dart';
|
|
import 'package:didvan/utils/category_labels.dart';
|
|
import 'package:didvan/views/widgets/didvan/text.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';
|
|
|
|
/// کارت اینفوگرافی — دقیقا کپی از PodcastListCard (تب پادکست در media page).
|
|
/// تفاوتها:
|
|
/// * بهجای آیکن clock + مدتزمان، آیکن puzzle + دستهی موضوعی نمایش داده میشود.
|
|
/// * onTap بهجای navigation به صفحهی پادکست، تصویر را در InteractiveViewer باز میکند.
|
|
class InfographyItem extends StatelessWidget {
|
|
final String image;
|
|
final String title;
|
|
final String category;
|
|
|
|
/// کلید English از metadata.category (مثلا 'political'). اگر داده شود،
|
|
/// بهجای `category` ـ والد، با ترجمهی فارسی نمایش داده میشود.
|
|
final String? metaCategory;
|
|
|
|
final List<Tag> tag;
|
|
final String createdAt;
|
|
final int id;
|
|
final bool marked;
|
|
final bool isLastItem;
|
|
|
|
/// UUID محتوا در content-service. اگر null/خالی باشه به جای navigation به
|
|
/// صفحهی detail، تصویر در InteractiveViewer باز میشه (سازگاری با آیتمهای legacy).
|
|
final String? keycloakId;
|
|
|
|
/// لینک خارجی (اختیاری). به صفحهی detail پاس داده میشه تا دکمهی «مشاهده منبع» نشون داده شه.
|
|
final String? link;
|
|
|
|
// ignore: unused_field
|
|
final bool liked;
|
|
// ignore: unused_field
|
|
final int likes;
|
|
final void Function(int id, bool value, bool shouldUpdate) onMarkChanged;
|
|
// ignore: unused_field
|
|
final void Function(int id, bool value, bool shouldUpdate) onLikedChanged;
|
|
|
|
const InfographyItem({
|
|
super.key,
|
|
required this.onMarkChanged,
|
|
required this.image,
|
|
required this.category,
|
|
required this.title,
|
|
required this.tag,
|
|
required this.createdAt,
|
|
required this.id,
|
|
required this.marked,
|
|
required this.liked,
|
|
required this.likes,
|
|
required this.onLikedChanged,
|
|
this.metaCategory,
|
|
this.isLastItem = false,
|
|
this.keycloakId,
|
|
this.link,
|
|
});
|
|
|
|
void _openDetail(BuildContext context) {
|
|
Navigator.of(context).pushNamed(
|
|
Routes.infographyDetails,
|
|
arguments: {
|
|
'item': inf.Content(
|
|
id: id,
|
|
marked: marked,
|
|
liked: liked,
|
|
likes: likes,
|
|
createdAt: createdAt,
|
|
title: title,
|
|
image: image,
|
|
category: category,
|
|
tags: tag,
|
|
metaCategory: metaCategory ?? '',
|
|
keycloakId: keycloakId,
|
|
link: link,
|
|
),
|
|
if (keycloakId != null && keycloakId!.isNotEmpty)
|
|
'contentUuid': keycloakId,
|
|
'onMarkChanged': (int id, bool value) =>
|
|
onMarkChanged(id, value, true),
|
|
},
|
|
);
|
|
}
|
|
|
|
String _displayCategory() {
|
|
final m = metaCategory;
|
|
if (m != null && m.isNotEmpty) {
|
|
final t = CategoryLabels.label(m);
|
|
if (t.isNotEmpty) return t;
|
|
}
|
|
return category;
|
|
}
|
|
|
|
String _formatDate(String dateStr) {
|
|
try {
|
|
final date = DateTime.parse(dateStr);
|
|
return date.toPersianDateStr();
|
|
} catch (e) {
|
|
return dateStr;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final textTheme = Theme.of(context).textTheme;
|
|
|
|
return GestureDetector(
|
|
onTap: () => _openDetail(context),
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
height: 120,
|
|
margin: const EdgeInsets.only(
|
|
bottom: 8,
|
|
left: 16,
|
|
right: 16,
|
|
),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(6.0),
|
|
child: AspectRatio(
|
|
aspectRatio: 1.42,
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(16),
|
|
// برای اینفوگرافی کل تصویر باید پیدا باشد (هیچ
|
|
// بخشی crop نشود). `BoxFit.contain` ضمن حفظ نسبت،
|
|
// کل تصویر را در container مینشاند؛ شاید کنارههای
|
|
// خالی بماند ولی هیچ بخشی از تصویر گم نمیشود.
|
|
child: SkeletonImage(
|
|
imageUrl: image,
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
borderRadius: BorderRadius.circular(16),
|
|
fit: BoxFit.contain,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(12, 3, 12, 10),
|
|
child: Container(
|
|
color: DesignConfig.isDark
|
|
? Colors.transparent
|
|
: Colors.white,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
children: [
|
|
DidvanText(
|
|
title,
|
|
style: textTheme.bodyMedium?.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
color: DesignConfig.isDark
|
|
? const Color.fromARGB(255, 206, 206, 206)
|
|
: Colors.black87,
|
|
),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const Spacer(),
|
|
Row(
|
|
children: [
|
|
SvgPicture.asset(
|
|
'lib/assets/icons/calendar.svg',
|
|
color: Theme.of(context).colorScheme.caption,
|
|
),
|
|
const SizedBox(width: 4),
|
|
DidvanText(
|
|
_formatDate(createdAt),
|
|
style: textTheme.bodySmall?.copyWith(
|
|
color:
|
|
Theme.of(context).colorScheme.caption,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 7),
|
|
Row(
|
|
children: [
|
|
// بهجای آیکن ساعت (مدت زمان پادکست)، آیکن
|
|
// دستهبندی + نام فارسی دسته نمایش داده میشود.
|
|
SvgPicture.asset(
|
|
'lib/assets/icons/element-2.svg',
|
|
color: Theme.of(context).colorScheme.caption,
|
|
width: 16,
|
|
height: 16,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Expanded(
|
|
child: DidvanText(
|
|
_displayCategory().toPersianDigit(),
|
|
style: textTheme.bodySmall?.copyWith(
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.caption,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (!isLastItem)
|
|
Divider(
|
|
color: Colors.grey[300],
|
|
thickness: 2,
|
|
indent: 16,
|
|
endIndent: 16,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|