diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 0e555642..c5d3e478 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -628,15 +628,15 @@ class _DiscoveryActivityItem extends StatelessWidget { children: [ for (final item in items) switch (type) { - 'realm' => RealmCard( + 'realm' => RealmDiscoveryCard( realm: SnRealm.fromJson(item['data']), maxWidth: 280, ), - 'publisher' => PublisherCard( + 'publisher' => PublisherDiscoveryCard( publisher: SnPublisher.fromJson(item['data']), maxWidth: 280, ), - 'article' => WebArticleCard( + 'article' => WebArticleDiscoveryCard( article: SnWebArticle.fromJson(item['data']), maxWidth: 280, ), diff --git a/lib/widgets/publisher/publisher_card.dart b/lib/widgets/publisher/publisher_card.dart index 0d312426..27593c0d 100644 --- a/lib/widgets/publisher/publisher_card.dart +++ b/lib/widgets/publisher/publisher_card.dart @@ -5,11 +5,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/publisher.dart'; import 'package:island/widgets/content/cloud_files.dart'; -class PublisherCard extends ConsumerWidget { +class PublisherDiscoveryCard extends ConsumerWidget { final SnPublisher publisher; final double? maxWidth; - const PublisherCard({super.key, required this.publisher, this.maxWidth}); + const PublisherDiscoveryCard({ + super.key, + required this.publisher, + this.maxWidth, + }); @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/lib/widgets/realm/realm_card.dart b/lib/widgets/realm/realm_card.dart index 2d005204..674db6b8 100644 --- a/lib/widgets/realm/realm_card.dart +++ b/lib/widgets/realm/realm_card.dart @@ -6,21 +6,20 @@ import 'package:island/models/realm.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:material_symbols_icons/symbols.dart'; -class RealmCard extends ConsumerWidget { +class RealmDiscoveryCard extends ConsumerWidget { final SnRealm realm; final double? maxWidth; - const RealmCard({super.key, required this.realm, this.maxWidth}); + const RealmDiscoveryCard({super.key, required this.realm, this.maxWidth}); @override Widget build(BuildContext context, WidgetRef ref) { Widget imageWidget; if (realm.picture != null) { - imageWidget = - imageWidget = CloudImageWidget( - file: realm.background, - fit: BoxFit.cover, - ); + imageWidget = imageWidget = CloudImageWidget( + file: realm.background, + fit: BoxFit.cover, + ); } else { imageWidget = ColoredBox( color: Theme.of(context).colorScheme.secondaryContainer, diff --git a/lib/widgets/web_article_card.dart b/lib/widgets/web_article_card.dart index af369e06..0cf8bb9a 100644 --- a/lib/widgets/web_article_card.dart +++ b/lib/widgets/web_article_card.dart @@ -71,3 +71,140 @@ class WebArticleCard extends StatelessWidget { ); } } + +class WebArticleDiscoveryCard extends StatelessWidget { + final SnWebArticle article; + final double? maxWidth; + final bool showDetails; + + const WebArticleDiscoveryCard({ + super.key, + required this.article, + this.maxWidth, + this.showDetails = false, + }); + + void _onTap(BuildContext context) { + context.pushNamed('articleDetail', pathParameters: {'id': article.id}); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), + child: Card( + margin: EdgeInsets.zero, + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: () => _onTap(context), + child: AspectRatio( + aspectRatio: 16 / 9, + child: Stack( + fit: StackFit.expand, + children: [ + // Image or fallback + article.preview?.imageUrl != null + ? CachedNetworkImage( + imageUrl: article.preview!.imageUrl!, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + ) + : ColoredBox( + color: colorScheme.secondaryContainer, + child: const Center( + child: Icon( + Icons.article_outlined, + size: 48, + color: Colors.white, + ), + ), + ), + // Gradient overlay + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black.withOpacity(0.7), + ], + ), + ), + ), + // Title + Align( + alignment: Alignment.bottomLeft, + child: Container( + padding: const EdgeInsets.only( + left: 12, + right: 12, + bottom: 8, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + if (showDetails) + const SizedBox(height: 8) + else + Spacer(), + Text( + article.title, + style: theme.textTheme.titleSmall?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + height: 1.3, + ), + maxLines: showDetails ? 3 : 1, + overflow: TextOverflow.ellipsis, + ), + if (showDetails && + article.author?.isNotEmpty == true) ...[ + const SizedBox(height: 4), + Text( + article.author!, + style: TextStyle( + fontSize: 10, + color: Colors.white.withOpacity(0.9), + fontWeight: FontWeight.w500, + ), + ), + ], + if (showDetails) const Spacer(), + if (showDetails && article.publishedAt != null) ...[ + Text( + '${article.publishedAt!.formatSystem()} ยท ${article.publishedAt!.formatRelative(context)}', + style: const TextStyle( + fontSize: 9, + color: Colors.white70, + ), + ), + const SizedBox(height: 2), + ], + Text( + article.feed?.title ?? 'Unknown Source', + style: const TextStyle( + fontSize: 9, + color: Colors.white70, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +}