update content handling and improve UI for infographics and banners
This commit is contained in:
parent
c60c081f99
commit
61718baa54
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Build pipeline for the didvan app — clean, fetch deps, then release APK.
|
||||||
|
# Usage: .\build.ps1
|
||||||
|
# Stops at the first failure.
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
Write-Host '==> flutter clean' -ForegroundColor Cyan
|
||||||
|
flutter clean
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "flutter clean failed" }
|
||||||
|
|
||||||
|
Write-Host '==> flutter pub get' -ForegroundColor Cyan
|
||||||
|
flutter pub get
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "flutter pub get failed" }
|
||||||
|
|
||||||
|
Write-Host '==> flutter build apk --release' -ForegroundColor Cyan
|
||||||
|
flutter build apk --release
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "flutter build failed" }
|
||||||
|
|
||||||
|
$apk = 'build\app\outputs\flutter-apk\app-release.apk'
|
||||||
|
if (Test-Path $apk) {
|
||||||
|
$sizeMb = [math]::Round((Get-Item $apk).Length / 1MB, 1)
|
||||||
|
Write-Host "==> Done. APK at $apk ($sizeMb MB)" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host '==> Done, but expected APK not found.' -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
@ -168,6 +168,16 @@ class ContentItem {
|
||||||
String? audioIdLocal;
|
String? audioIdLocal;
|
||||||
String? videoIdLocal;
|
String? videoIdLocal;
|
||||||
int? mediaDuration;
|
int? mediaDuration;
|
||||||
|
|
||||||
|
// اگر content-service خودش coverImage signed-url را در پاسخ گذاشته
|
||||||
|
// (resolveFileUrl برای metadata.cover_image_id)، آن را بهعنوان منبع
|
||||||
|
// اصلی بپذیر — این مسیر برای محتواهایی مثل «بنر فولادینفو» که body
|
||||||
|
// خالی است و تصویر فقط در metadata.cover_image_id ذخیره میشود لازم
|
||||||
|
// است. اسکن body در ادامه فقط در صورت خالیماندنِ cover کار میکند.
|
||||||
|
final rootCover = json['coverImage'];
|
||||||
|
if (rootCover is String && rootCover.isNotEmpty) {
|
||||||
|
cover = rootCover;
|
||||||
|
}
|
||||||
// helper: تشخیصِ شناسهی UUID از یک URL واقعی.
|
// helper: تشخیصِ شناسهی UUID از یک URL واقعی.
|
||||||
bool isHttpUrl(String s) => s.startsWith('http://') || s.startsWith('https://');
|
bool isHttpUrl(String s) => s.startsWith('http://') || s.startsWith('https://');
|
||||||
bool looksLikeUuid(String s) => RegExp(
|
bool looksLikeUuid(String s) => RegExp(
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,20 @@ import 'package:flutter/cupertino.dart';
|
||||||
class CoreProvier with ChangeNotifier {
|
class CoreProvier with ChangeNotifier {
|
||||||
AppState _appState = AppState.busy;
|
AppState _appState = AppState.busy;
|
||||||
|
|
||||||
|
/// وقتی provider dispose میشود این flag true میشود تا callback های
|
||||||
|
/// async (مثل fetch network) که بعد از dispose برمیگردند و `update()` را
|
||||||
|
/// صدا میزنند به جای throw، silently no-op شوند.
|
||||||
|
bool _disposed = false;
|
||||||
|
bool get isDisposed => _disposed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_disposed = true;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
set appState(AppState newState) {
|
set appState(AppState newState) {
|
||||||
|
if (_disposed) return;
|
||||||
if (newState == AppState.isolatedBusy) {
|
if (newState == AppState.isolatedBusy) {
|
||||||
ActionSheetUtils(navigatorKey.currentContext!).showLogoLoadingIndicator();
|
ActionSheetUtils(navigatorKey.currentContext!).showLogoLoadingIndicator();
|
||||||
}
|
}
|
||||||
|
|
@ -17,7 +30,10 @@ class CoreProvier with ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void update() => notifyListeners();
|
void update() {
|
||||||
|
if (_disposed) return;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
AppState get appState => _appState;
|
AppState get appState => _appState;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -193,35 +193,48 @@ class _InfographyDetailsState extends State<InfographyDetails> {
|
||||||
children: [
|
children: [
|
||||||
LayoutBuilder(
|
LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final double imageHeight = constraints.maxWidth / (16 / 9);
|
|
||||||
const double overlap = 24.0;
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
padding: const EdgeInsets.only(bottom: 92),
|
padding: const EdgeInsets.only(bottom: 92),
|
||||||
child: Stack(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// === تصویر اصلی ===
|
// === تصویر اصلی — کل تصویر پیدا، ارتفاع طبیعی ===
|
||||||
Positioned(
|
// بهجای container با ارتفاع 16:9 fixed (که portrait
|
||||||
top: 0,
|
// را crop میکرد یا با contain کناره خالی میگذاشت)،
|
||||||
left: 0,
|
// اینجا از Image.network مستقیم استفاده میکنیم تا
|
||||||
right: 0,
|
// ارتفاع تصویر متناسب با aspect ratio طبیعیاش باشد.
|
||||||
height: imageHeight,
|
GestureDetector(
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => ActionSheetUtils(context)
|
onTap: () => ActionSheetUtils(context)
|
||||||
.openInteractiveViewer(context, item.image, false),
|
.openInteractiveViewer(context, item.image, false),
|
||||||
child: SkeletonImage(
|
child: Container(
|
||||||
imageUrl: item.image,
|
color: theme.colorScheme.background,
|
||||||
borderRadius: BorderRadius.zero,
|
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
child: Image.network(
|
||||||
|
item.image,
|
||||||
|
width: double.infinity,
|
||||||
|
fit: BoxFit.fitWidth,
|
||||||
|
errorBuilder: (_, __, ___) => SizedBox(
|
||||||
|
height: constraints.maxWidth * 9 / 16,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.image_not_supported_outlined),
|
||||||
|
),
|
||||||
|
loadingBuilder: (context, child, progress) {
|
||||||
|
if (progress == null) return child;
|
||||||
|
return SizedBox(
|
||||||
|
height: constraints.maxWidth * 9 / 16,
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// === کارت سفید روی تصویر، لبهی بالای آن «صاف» ===
|
// === کارت سفید زیر تصویر (overlap حذف شد) ===
|
||||||
Container(
|
Container(
|
||||||
margin: EdgeInsets.only(top: imageHeight - overlap),
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.background,
|
color: theme.colorScheme.background,
|
||||||
// لبه صاف بهجای BorderRadius.vertical(top: 24)
|
// لبه صاف بهجای BorderRadius.vertical(top: 24)
|
||||||
|
|
|
||||||
|
|
@ -195,38 +195,6 @@ class _InfographyScreenState extends State<InfographyScreen> {
|
||||||
value: context.watch<InfographyScreenState>().lastSearch,
|
value: context.watch<InfographyScreenState>().lastSearch,
|
||||||
isFiltered: context.watch<InfographyScreenState>().filtering),
|
isFiltered: context.watch<InfographyScreenState>().filtering),
|
||||||
),
|
),
|
||||||
// پوستر بالای صفحه — از دستهی «بنر فولادینفو» کشیده میشود.
|
|
||||||
// اگر هیچ آیتم منتشرشدهای در آن دسته نباشد، چیزی نمایش داده نمیشود.
|
|
||||||
Builder(builder: (context) {
|
|
||||||
final bannerUrl =
|
|
||||||
context.watch<InfographyScreenState>().topBannerImageUrl;
|
|
||||||
if (bannerUrl == null || bannerUrl.isEmpty) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 16 / 7,
|
|
||||||
child: Image.network(
|
|
||||||
bannerUrl,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (_, __, ___) => const SizedBox.shrink(),
|
|
||||||
loadingBuilder: (context, child, progress) {
|
|
||||||
if (progress == null) return child;
|
|
||||||
return Container(
|
|
||||||
color: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.surface
|
|
||||||
.withOpacity(0.4),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
StateHandler<InfographyScreenState>(
|
StateHandler<InfographyScreenState>(
|
||||||
placeholder: SingleChildScrollView(
|
placeholder: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
|
||||||
|
|
@ -72,15 +72,6 @@ class InfographyScreenState extends CoreProvier {
|
||||||
static const String _infographyCategoryId =
|
static const String _infographyCategoryId =
|
||||||
'3e53627d-9a18-4bad-b4ae-1f0c55438147';
|
'3e53627d-9a18-4bad-b4ae-1f0c55438147';
|
||||||
|
|
||||||
/// شناسهی دستهی «بنر فولادینفو» — تصویر کاور آخرین content این دسته
|
|
||||||
/// بهعنوان پوستر بالای صفحهی فولادینفو در اپ نمایش داده میشود.
|
|
||||||
static const String _foladinfoBannerCategoryId =
|
|
||||||
'd1a5e9b3-6f47-4c82-9e1d-3b8f2c5a4d76';
|
|
||||||
|
|
||||||
/// URL تصویر بنر فولادینفو که در بالای صفحه نمایش داده میشود؛
|
|
||||||
/// اگر null یا خالی بود، بنر اصلاً رندر نمیشود.
|
|
||||||
String? topBannerImageUrl;
|
|
||||||
|
|
||||||
/// فقط محتوای منتشرشده/تأییدشده. status فقط IN دارد.
|
/// فقط محتوای منتشرشده/تأییدشده. status فقط IN دارد.
|
||||||
static const String _visibleStatusFilter = '\$in:accepted,published';
|
static const String _visibleStatusFilter = '\$in:accepted,published';
|
||||||
|
|
||||||
|
|
@ -176,39 +167,6 @@ class InfographyScreenState extends CoreProvier {
|
||||||
appState = AppState.idle;
|
appState = AppState.idle;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// آخرین content منتشرشده در دستهی «بنر فولادینفو» را میگیریم و
|
|
||||||
/// `coverImage` آن را در [topBannerImageUrl] میگذاریم. در صورت خطا
|
|
||||||
/// یا خالیبودن، بنر نمایش داده نمیشود (مقدار null میماند).
|
|
||||||
Future<void> _fetchTopBanner() async {
|
|
||||||
try {
|
|
||||||
final res = await ContentService.instance.getContents(
|
|
||||||
page: 1,
|
|
||||||
limit: 1,
|
|
||||||
filterStatus: _visibleStatusFilter,
|
|
||||||
categoryId: _foladinfoBannerCategoryId,
|
|
||||||
sortBy: const [MapEntry('createdAt', 'DESC')],
|
|
||||||
);
|
|
||||||
if (!res.isSuccess || res.data == null || res.data!.data.isEmpty) {
|
|
||||||
topBannerImageUrl = null;
|
|
||||||
update();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final first = res.data!.data.first;
|
|
||||||
// The list endpoint may already include coverImage; if not, fetch detail.
|
|
||||||
String? cover = first.coverImage;
|
|
||||||
if (cover == null || cover.isEmpty) {
|
|
||||||
final detail = await ContentService.instance.getContent(first.id);
|
|
||||||
if (detail.isSuccess && detail.data != null) {
|
|
||||||
cover = detail.data!.coverImage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
topBannerImageUrl = (cover != null && cover.isNotEmpty) ? cover : null;
|
|
||||||
update();
|
|
||||||
} catch (_) {
|
|
||||||
topBannerImageUrl = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void resetFilters(bool isInit) {
|
void resetFilters(bool isInit) {
|
||||||
selectedCats.clear();
|
selectedCats.clear();
|
||||||
selectedTags.clear();
|
selectedTags.clear();
|
||||||
|
|
@ -226,7 +184,6 @@ class InfographyScreenState extends CoreProvier {
|
||||||
resetFilters(true);
|
resetFilters(true);
|
||||||
Future.delayed(Duration.zero, () {
|
Future.delayed(Duration.zero, () {
|
||||||
getInfographyContent(page: 1);
|
getInfographyContent(page: 1);
|
||||||
_fetchTopBanner();
|
|
||||||
});
|
});
|
||||||
categories = [
|
categories = [
|
||||||
CategoryData(
|
CategoryData(
|
||||||
|
|
|
||||||
|
|
@ -109,11 +109,13 @@ class MainPageState extends CoreProvier {
|
||||||
_getSwotItems(),
|
_getSwotItems(),
|
||||||
_getDidvanPlus(),
|
_getDidvanPlus(),
|
||||||
_getDidvanVoice(),
|
_getDidvanVoice(),
|
||||||
|
// Top banner از category «بنر فولادینفو» در content-service خوانده
|
||||||
|
// میشود (مستقل از legacy api) — قبلاً پشت useLegacyApi پنهان بود.
|
||||||
|
_getTopBanner(),
|
||||||
];
|
];
|
||||||
if (AuthConfig.useLegacyApi) {
|
if (AuthConfig.useLegacyApi) {
|
||||||
tasks.addAll([
|
tasks.addAll([
|
||||||
_getMainPageContent(),
|
_getMainPageContent(),
|
||||||
_getTopBanner(),
|
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
|
|
@ -220,9 +222,11 @@ class MainPageState extends CoreProvier {
|
||||||
|
|
||||||
Future<void> _loadFooladInfoBanners() async {
|
Future<void> _loadFooladInfoBanners() async {
|
||||||
try {
|
try {
|
||||||
|
// فقط آخرین (تازهترین) بنر فولادینفو نشان داده میشود — یعنی هیچ
|
||||||
|
// carousel چرخشی نباشد. limit=1 + DESC = جدیدترین.
|
||||||
final res = await ContentService.instance.getContents(
|
final res = await ContentService.instance.getContents(
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 15,
|
limit: 1,
|
||||||
filterStatus: _visibleStatusFilter,
|
filterStatus: _visibleStatusFilter,
|
||||||
categoryId: _fooladInfoCategoryId,
|
categoryId: _fooladInfoCategoryId,
|
||||||
sortBy: const [MapEntry('createdAt', 'DESC')],
|
sortBy: const [MapEntry('createdAt', 'DESC')],
|
||||||
|
|
@ -685,22 +689,50 @@ class MainPageState extends CoreProvier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(backend-migration): `api.didvan.app/topBanner` فعلا در دسترس نیست.
|
/// شناسهی دستهی «بنر فولادینفو» در content-service. آخرین آیتم منتشرشده
|
||||||
// پس از پیادهسازی endpoint معادل در api2.didvan.com، این تابع را به آن
|
/// در این دسته بهعنوان پوستر بالای صفحهی فولادینفو استفاده میشود.
|
||||||
// متصل کنید.
|
static const String _foladinfoBannerCategoryId =
|
||||||
|
'd1a5e9b3-6f47-4c82-9e1d-3b8f2c5a4d76';
|
||||||
|
|
||||||
|
/// تاپبنر فولادینفو از content-service خوانده میشود (آخرین content
|
||||||
|
/// منتشرشده در دستهی «بنر فولادینفو»). `coverImage` این آیتم سرور-ساید
|
||||||
|
/// از `metadata.cover_image_id` به signed S3 URL تبدیل میشود.
|
||||||
Future<void> _getTopBanner() async {
|
Future<void> _getTopBanner() async {
|
||||||
|
print('[FOLADINFO_TOPBANNER] start');
|
||||||
try {
|
try {
|
||||||
final service = RequestService(RequestHelper.topBanner);
|
final res = await ContentService.instance.getContents(
|
||||||
await service.httpGet().timeout(_legacyTimeout);
|
page: 1,
|
||||||
if (service.statusCode == 200) {
|
limit: 1,
|
||||||
final data = service.result['result'] ?? service.result;
|
filterStatus: r'$in:accepted,published',
|
||||||
topBanner = TopBannerModel.fromJson(data);
|
categoryId: _foladinfoBannerCategoryId,
|
||||||
|
sortBy: const [MapEntry('createdAt', 'DESC')],
|
||||||
|
);
|
||||||
|
print(
|
||||||
|
'[FOLADINFO_TOPBANNER] success=${res.isSuccess} len=${res.data?.data.length}');
|
||||||
|
if (!res.isSuccess || res.data == null || res.data!.data.isEmpty) {
|
||||||
|
topBanner = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} on TimeoutException {
|
final first = res.data!.data.first;
|
||||||
debugPrint('⏱️ topBanner legacy timeout');
|
String? cover = first.coverImage;
|
||||||
} catch (e) {
|
if (cover == null || cover.isEmpty) {
|
||||||
debugPrint('❌ topBanner legacy failed: $e');
|
// fallback: backend ممکن است coverImage را در list نگذارد؛ detail
|
||||||
|
// را میگیریم چون findOne بهطور حتم signed URL را پر میکند.
|
||||||
|
final detail = await ContentService.instance.getContent(first.id);
|
||||||
|
if (detail.isSuccess && detail.data != null) {
|
||||||
|
cover = detail.data!.coverImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print('[FOLADINFO_TOPBANNER] cover=$cover');
|
||||||
|
if (cover != null && cover.isNotEmpty) {
|
||||||
|
topBanner = TopBannerModel(id: 0, image: cover);
|
||||||
|
} else {
|
||||||
|
topBanner = null;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e, st) {
|
||||||
|
print('[FOLADINFO_TOPBANNER] ERROR: $e\n$st');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -138,11 +138,16 @@ class InfographyItem extends StatelessWidget {
|
||||||
aspectRatio: 1.42,
|
aspectRatio: 1.42,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
// برای اینفوگرافی کل تصویر باید پیدا باشد (هیچ
|
||||||
|
// بخشی crop نشود). `BoxFit.contain` ضمن حفظ نسبت،
|
||||||
|
// کل تصویر را در container مینشاند؛ شاید کنارههای
|
||||||
|
// خالی بماند ولی هیچ بخشی از تصویر گم نمیشود.
|
||||||
child: SkeletonImage(
|
child: SkeletonImage(
|
||||||
imageUrl: image,
|
imageUrl: image,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -27,26 +27,55 @@ class _MediaBannerWithThumbnailsState extends State<MediaBannerWithThumbnails> {
|
||||||
final state = context.watch<MainPageState>();
|
final state = context.watch<MainPageState>();
|
||||||
// بنرِ فولادینفو از content-service (دستهی «تصویر») میآید.
|
// بنرِ فولادینفو از content-service (دستهی «تصویر») میآید.
|
||||||
final banners = state.fooladInfoBanners;
|
final banners = state.fooladInfoBanners;
|
||||||
|
final hasTopBanner = widget.isFirst && state.topBanner != null;
|
||||||
|
|
||||||
if (banners.isEmpty) {
|
if (banners.isEmpty && !hasTopBanner) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
if (widget.isFirst && state.topBanner != null) ...[
|
if (hasTopBanner) ...[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 5),
|
padding: const EdgeInsets.symmetric(horizontal: 5),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 16 / 9,
|
||||||
child: SkeletonImage(
|
child: SkeletonImage(
|
||||||
imageUrl: state.topBanner!.image,
|
imageUrl: state.topBanner!.image,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
],
|
],
|
||||||
|
// وقتی فقط ۱ بنر داریم، dots و thumbnails navigation معنا ندارد و
|
||||||
|
// پنهان میشوند؛ فقط همان یک تصویر نمایش داده میشود.
|
||||||
|
if (banners.length == 1) ...[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 5),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => banners.first.link == null ||
|
||||||
|
banners.first.link!.isEmpty
|
||||||
|
? ActionSheetUtils(context).openInteractiveViewer(
|
||||||
|
context, banners.first.image, false)
|
||||||
|
: launchUrlString(banners.first.link!,
|
||||||
|
mode: LaunchMode.inAppWebView),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 16 / 9,
|
||||||
|
child: SkeletonImage(
|
||||||
|
imageUrl: banners.first.image,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else if (banners.length > 1) ...[
|
||||||
CarouselSlider.builder(
|
CarouselSlider.builder(
|
||||||
carouselController: _carouselController,
|
carouselController: _carouselController,
|
||||||
itemCount: banners.length,
|
itemCount: banners.length,
|
||||||
|
|
@ -263,6 +292,7 @@ class _MediaBannerWithThumbnailsState extends State<MediaBannerWithThumbnails> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,11 @@ class SkeletonImage extends StatelessWidget {
|
||||||
final double? pHeight;
|
final double? pHeight;
|
||||||
final BorderRadius? borderRadius;
|
final BorderRadius? borderRadius;
|
||||||
final double? aspectRatio;
|
final double? aspectRatio;
|
||||||
|
|
||||||
|
/// نحوهی fit کردن تصویر در container. مقدار پیشفرض `BoxFit.cover` است
|
||||||
|
/// (برای کارتهای سبک پادکست/خبر). برای صفحههایی مثل اینفوگرافی که
|
||||||
|
/// میخواهیم کل تصویر پیدا باشد، `BoxFit.contain` پاس داده میشود.
|
||||||
|
final BoxFit? fit;
|
||||||
const SkeletonImage({
|
const SkeletonImage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.imageUrl,
|
required this.imageUrl,
|
||||||
|
|
@ -26,6 +31,7 @@ class SkeletonImage extends StatelessWidget {
|
||||||
this.height,
|
this.height,
|
||||||
this.pWidth,
|
this.pWidth,
|
||||||
this.pHeight,
|
this.pHeight,
|
||||||
|
this.fit,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -60,7 +66,7 @@ class SkeletonImage extends StatelessWidget {
|
||||||
child: const Icon(Icons.image_not_supported_outlined));
|
child: const Icon(Icons.image_not_supported_outlined));
|
||||||
},
|
},
|
||||||
errorListener: (value) {},
|
errorListener: (value) {},
|
||||||
fit: kIsWeb ? BoxFit.fill : BoxFit.cover,
|
fit: fit ?? (kIsWeb ? BoxFit.fill : BoxFit.cover),
|
||||||
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
|
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
|
||||||
httpHeaders: headers,
|
httpHeaders: headers,
|
||||||
width: width,
|
width: width,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue