210 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			210 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'package:flutter/material.dart';
 | 
						|
import 'package:gap/gap.dart';
 | 
						|
import 'package:island/models/embed.dart';
 | 
						|
import 'package:island/widgets/content/image.dart';
 | 
						|
import 'package:material_symbols_icons/symbols.dart';
 | 
						|
import 'package:url_launcher/url_launcher.dart';
 | 
						|
 | 
						|
class EmbedLinkWidget extends StatelessWidget {
 | 
						|
  final SnScrappedLink link;
 | 
						|
  final double? maxWidth;
 | 
						|
  final EdgeInsetsGeometry? margin;
 | 
						|
 | 
						|
  const EmbedLinkWidget({
 | 
						|
    super.key,
 | 
						|
    required this.link,
 | 
						|
    this.maxWidth,
 | 
						|
    this.margin,
 | 
						|
  });
 | 
						|
 | 
						|
  Future<void> _launchUrl() async {
 | 
						|
    final uri = Uri.parse(link.url);
 | 
						|
    if (await canLaunchUrl(uri)) {
 | 
						|
      await launchUrl(uri, mode: LaunchMode.externalApplication);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    final theme = Theme.of(context);
 | 
						|
    final colorScheme = theme.colorScheme;
 | 
						|
 | 
						|
    return Container(
 | 
						|
      width: maxWidth,
 | 
						|
      margin: margin ?? const EdgeInsets.symmetric(vertical: 8),
 | 
						|
      child: Card(
 | 
						|
        margin: EdgeInsets.zero,
 | 
						|
        clipBehavior: Clip.antiAlias,
 | 
						|
        child: InkWell(
 | 
						|
          onTap: _launchUrl,
 | 
						|
          child: Column(
 | 
						|
            crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
            children: [
 | 
						|
              // Preview Image
 | 
						|
              if (link.imageUrl != null && link.imageUrl!.isNotEmpty)
 | 
						|
                AspectRatio(
 | 
						|
                  aspectRatio: 16 / 9,
 | 
						|
                  child: UniversalImage(uri: link.imageUrl!, fit: BoxFit.cover),
 | 
						|
                ),
 | 
						|
 | 
						|
              // Content
 | 
						|
              Padding(
 | 
						|
                padding: const EdgeInsets.all(16),
 | 
						|
                child: Column(
 | 
						|
                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
                  children: [
 | 
						|
                    // Site info row
 | 
						|
                    Row(
 | 
						|
                      children: [
 | 
						|
                        // Favicon
 | 
						|
                        if (link.faviconUrl?.isNotEmpty ?? false) ...[
 | 
						|
                          ClipRRect(
 | 
						|
                            borderRadius: BorderRadius.circular(4),
 | 
						|
                            child: UniversalImage(
 | 
						|
                              uri: link.faviconUrl!,
 | 
						|
                              width: 16,
 | 
						|
                              height: 16,
 | 
						|
                              fit: BoxFit.cover,
 | 
						|
                            ),
 | 
						|
                          ),
 | 
						|
                          const Gap(8),
 | 
						|
                        ] else ...[
 | 
						|
                          Icon(
 | 
						|
                            Symbols.link,
 | 
						|
                            size: 16,
 | 
						|
                            color: colorScheme.onSurfaceVariant,
 | 
						|
                          ),
 | 
						|
                          const Gap(8),
 | 
						|
                        ],
 | 
						|
 | 
						|
                        // Site name
 | 
						|
                        Expanded(
 | 
						|
                          child: Text(
 | 
						|
                            (link.siteName?.isNotEmpty ?? false)
 | 
						|
                                ? link.siteName!
 | 
						|
                                : Uri.parse(link.url).host,
 | 
						|
                            style: theme.textTheme.bodySmall?.copyWith(
 | 
						|
                              color: colorScheme.onSurfaceVariant,
 | 
						|
                            ),
 | 
						|
                            maxLines: 1,
 | 
						|
                            overflow: TextOverflow.ellipsis,
 | 
						|
                          ),
 | 
						|
                        ),
 | 
						|
 | 
						|
                        // External link icon
 | 
						|
                        Icon(
 | 
						|
                          Symbols.open_in_new,
 | 
						|
                          size: 16,
 | 
						|
                          color: colorScheme.onSurfaceVariant,
 | 
						|
                        ),
 | 
						|
                      ],
 | 
						|
                    ),
 | 
						|
 | 
						|
                    const Gap(8),
 | 
						|
 | 
						|
                    // Title
 | 
						|
                    if (link.title.isNotEmpty) ...[
 | 
						|
                      Text(
 | 
						|
                        link.title,
 | 
						|
                        style: theme.textTheme.titleMedium?.copyWith(
 | 
						|
                          fontWeight: FontWeight.w600,
 | 
						|
                        ),
 | 
						|
                        maxLines: 2,
 | 
						|
                        overflow: TextOverflow.ellipsis,
 | 
						|
                      ),
 | 
						|
                      const Gap(4),
 | 
						|
                    ],
 | 
						|
 | 
						|
                    // Description
 | 
						|
                    if (link.description != null &&
 | 
						|
                        link.description!.isNotEmpty) ...[
 | 
						|
                      Text(
 | 
						|
                        link.description!,
 | 
						|
                        style: theme.textTheme.bodyMedium?.copyWith(
 | 
						|
                          color: colorScheme.onSurfaceVariant,
 | 
						|
                        ),
 | 
						|
                        maxLines: 3,
 | 
						|
                        overflow: TextOverflow.ellipsis,
 | 
						|
                      ),
 | 
						|
                      const Gap(8),
 | 
						|
                    ],
 | 
						|
 | 
						|
                    // URL
 | 
						|
                    Text(
 | 
						|
                      link.url,
 | 
						|
                      style: theme.textTheme.bodySmall?.copyWith(
 | 
						|
                        color: colorScheme.primary,
 | 
						|
                        decoration: TextDecoration.underline,
 | 
						|
                      ),
 | 
						|
                      maxLines: 1,
 | 
						|
                      overflow: TextOverflow.ellipsis,
 | 
						|
                    ),
 | 
						|
 | 
						|
                    // Author and publish date
 | 
						|
                    if (link.author != null || link.publishedDate != null) ...[
 | 
						|
                      const Gap(8),
 | 
						|
                      Row(
 | 
						|
                        children: [
 | 
						|
                          if (link.author != null) ...[
 | 
						|
                            Icon(
 | 
						|
                              Symbols.person,
 | 
						|
                              size: 14,
 | 
						|
                              color: colorScheme.onSurfaceVariant,
 | 
						|
                            ),
 | 
						|
                            const Gap(4),
 | 
						|
                            Text(
 | 
						|
                              link.author!,
 | 
						|
                              style: theme.textTheme.bodySmall?.copyWith(
 | 
						|
                                color: colorScheme.onSurfaceVariant,
 | 
						|
                              ),
 | 
						|
                            ),
 | 
						|
                          ],
 | 
						|
                          if (link.author != null && link.publishedDate != null)
 | 
						|
                            const Gap(16),
 | 
						|
                          if (link.publishedDate != null) ...[
 | 
						|
                            Icon(
 | 
						|
                              Symbols.schedule,
 | 
						|
                              size: 14,
 | 
						|
                              color: colorScheme.onSurfaceVariant,
 | 
						|
                            ),
 | 
						|
                            const Gap(4),
 | 
						|
                            Text(
 | 
						|
                              _formatDate(link.publishedDate!),
 | 
						|
                              style: theme.textTheme.bodySmall?.copyWith(
 | 
						|
                                color: colorScheme.onSurfaceVariant,
 | 
						|
                              ),
 | 
						|
                            ),
 | 
						|
                          ],
 | 
						|
                        ],
 | 
						|
                      ),
 | 
						|
                    ],
 | 
						|
                  ],
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ],
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  String _formatDate(DateTime date) {
 | 
						|
    try {
 | 
						|
      final now = DateTime.now();
 | 
						|
      final difference = now.difference(date);
 | 
						|
 | 
						|
      if (difference.inDays == 0) {
 | 
						|
        return 'Today';
 | 
						|
      } else if (difference.inDays == 1) {
 | 
						|
        return 'Yesterday';
 | 
						|
      } else if (difference.inDays < 7) {
 | 
						|
        return '${difference.inDays} days ago';
 | 
						|
      } else {
 | 
						|
        return '${date.day}/${date.month}/${date.year}';
 | 
						|
      }
 | 
						|
    } catch (e) {
 | 
						|
      return date.toString();
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 |