💄 Optimize the link embed
This commit is contained in:
		@@ -362,7 +362,10 @@ class ServerStateNotifier extends StateNotifier<ServerState> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void setCurrentActivity(String? id, Map<String, dynamic>? data) {
 | 
			
		||||
    state = state.copyWith(currentActivityManualId: id, currentActivityData: data);
 | 
			
		||||
    state = state.copyWith(
 | 
			
		||||
      currentActivityManualId: id,
 | 
			
		||||
      currentActivityData: data,
 | 
			
		||||
    );
 | 
			
		||||
    if (id != null && data != null) {
 | 
			
		||||
      _startRenewal();
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -372,8 +375,10 @@ class ServerStateNotifier extends StateNotifier<ServerState> {
 | 
			
		||||
 | 
			
		||||
  void _startRenewal() {
 | 
			
		||||
    _renewalTimer?.cancel();
 | 
			
		||||
    const int renewalIntervalSeconds = kPresenseActivityLease * 60 - 30;
 | 
			
		||||
    _renewalTimer = Timer.periodic(Duration(seconds: renewalIntervalSeconds), (timer) {
 | 
			
		||||
    const int renewalIntervalSeconds = kPresenceActivityLease * 60 - 30;
 | 
			
		||||
    _renewalTimer = Timer.periodic(Duration(seconds: renewalIntervalSeconds), (
 | 
			
		||||
      timer,
 | 
			
		||||
    ) {
 | 
			
		||||
      _renewActivity();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
@@ -386,7 +391,10 @@ class ServerStateNotifier extends StateNotifier<ServerState> {
 | 
			
		||||
  Future<void> _renewActivity() async {
 | 
			
		||||
    if (state.currentActivityData != null) {
 | 
			
		||||
      try {
 | 
			
		||||
        await apiClient.post('/pass/activities', data: state.currentActivityData);
 | 
			
		||||
        await apiClient.post(
 | 
			
		||||
          '/pass/activities',
 | 
			
		||||
          data: state.currentActivityData,
 | 
			
		||||
        );
 | 
			
		||||
        talker.log('Activity lease renewed');
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        talker.log('Failed to renew activity lease: $e');
 | 
			
		||||
@@ -395,7 +403,7 @@ class ServerStateNotifier extends StateNotifier<ServerState> {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const kPresenseActivityLease = 5;
 | 
			
		||||
const kPresenceActivityLease = 5;
 | 
			
		||||
 | 
			
		||||
// Providers
 | 
			
		||||
final rpcServerStateProvider = StateNotifierProvider<
 | 
			
		||||
@@ -459,8 +467,10 @@ final rpcServerStateProvider = StateNotifierProvider<
 | 
			
		||||
            activity['details'] ?? activity['assets']?['large_text'];
 | 
			
		||||
        var imageSmall = activity['assets']?['small_image'];
 | 
			
		||||
        var imageLarge = activity['assets']?['large_image'];
 | 
			
		||||
        if (imageSmall != null && !imageSmall!.contains(':')) imageSmall = 'discord:$imageSmall';
 | 
			
		||||
        if (imageLarge != null && !imageLarge!.contains(':')) imageLarge = 'discord:$imageLarge';
 | 
			
		||||
        if (imageSmall != null && !imageSmall!.contains(':'))
 | 
			
		||||
          imageSmall = 'discord:$imageSmall';
 | 
			
		||||
        if (imageLarge != null && !imageLarge!.contains(':'))
 | 
			
		||||
          imageLarge = 'discord:$imageLarge';
 | 
			
		||||
        try {
 | 
			
		||||
          final apiClient = ref.watch(apiClientProvider);
 | 
			
		||||
          final activityData = {
 | 
			
		||||
@@ -474,7 +484,7 @@ final rpcServerStateProvider = StateNotifierProvider<
 | 
			
		||||
            'small_image': imageSmall,
 | 
			
		||||
            'large_image': imageLarge,
 | 
			
		||||
            'meta': activity,
 | 
			
		||||
            'lease_minutes': kPresenseActivityLease,
 | 
			
		||||
            'lease_minutes': kPresenceActivityLease,
 | 
			
		||||
          };
 | 
			
		||||
 | 
			
		||||
          await apiClient.post('/pass/activities', data: activityData);
 | 
			
		||||
 
 | 
			
		||||
@@ -147,6 +147,7 @@ class ExploreScreen extends HookConsumerWidget {
 | 
			
		||||
              tabAlignment: TabAlignment.start,
 | 
			
		||||
              isScrollable: true,
 | 
			
		||||
              dividerColor: Colors.transparent,
 | 
			
		||||
              labelPadding: const EdgeInsets.symmetric(horizontal: 12),
 | 
			
		||||
              tabs: [
 | 
			
		||||
                Tab(
 | 
			
		||||
                  icon: Tooltip(
 | 
			
		||||
@@ -392,6 +393,7 @@ class ExploreScreen extends HookConsumerWidget {
 | 
			
		||||
                tabAlignment: TabAlignment.start,
 | 
			
		||||
                isScrollable: true,
 | 
			
		||||
                dividerColor: Colors.transparent,
 | 
			
		||||
                labelPadding: const EdgeInsets.symmetric(horizontal: 12),
 | 
			
		||||
                tabs: [
 | 
			
		||||
                  Tab(
 | 
			
		||||
                    icon: Tooltip(
 | 
			
		||||
 
 | 
			
		||||
@@ -52,14 +52,14 @@ Future<String?> discordAssetsUrl(
 | 
			
		||||
  return null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const kPresenseActivityTypes = [
 | 
			
		||||
const kPresenceActivityTypes = [
 | 
			
		||||
  'unknown',
 | 
			
		||||
  'presenceTypeGaming',
 | 
			
		||||
  'presenceTypeMusic',
 | 
			
		||||
  'presenceTypeWorkout',
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const kPresenseActivityIcons = <IconData>[
 | 
			
		||||
const kPresenceActivityIcons = <IconData>[
 | 
			
		||||
  Symbols.question_mark_rounded,
 | 
			
		||||
  Symbols.play_arrow_rounded,
 | 
			
		||||
  Symbols.music_note_rounded,
 | 
			
		||||
@@ -200,10 +200,10 @@ class ActivityPresenceWidget extends ConsumerWidget {
 | 
			
		||||
                      Row(
 | 
			
		||||
                        children: [
 | 
			
		||||
                          Text(
 | 
			
		||||
                            kPresenseActivityTypes[activity.type],
 | 
			
		||||
                            kPresenceActivityTypes[activity.type],
 | 
			
		||||
                          ).tr().fontSize(11),
 | 
			
		||||
                          Icon(
 | 
			
		||||
                            kPresenseActivityIcons[activity.type],
 | 
			
		||||
                            kPresenceActivityIcons[activity.type],
 | 
			
		||||
                            size: 15,
 | 
			
		||||
                            fill: 1,
 | 
			
		||||
                          ),
 | 
			
		||||
@@ -298,9 +298,9 @@ class ActivityPresenceWidget extends ConsumerWidget {
 | 
			
		||||
                          Row(
 | 
			
		||||
                            spacing: 4,
 | 
			
		||||
                            children: [
 | 
			
		||||
                              Text(kPresenseActivityTypes[activity.type]).tr(),
 | 
			
		||||
                              Text(kPresenceActivityTypes[activity.type]).tr(),
 | 
			
		||||
                              Icon(
 | 
			
		||||
                                kPresenseActivityIcons[activity.type],
 | 
			
		||||
                                kPresenceActivityIcons[activity.type],
 | 
			
		||||
                                size: 16,
 | 
			
		||||
                                fill: 1,
 | 
			
		||||
                              ),
 | 
			
		||||
 
 | 
			
		||||
@@ -71,6 +71,9 @@ class AccountStatusCreationWidget extends HookConsumerWidget {
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final userStatus = ref.watch(accountStatusProvider(uname));
 | 
			
		||||
 | 
			
		||||
    final renderPadding =
 | 
			
		||||
        padding ?? EdgeInsets.symmetric(horizontal: 16, vertical: 8);
 | 
			
		||||
 | 
			
		||||
    return InkWell(
 | 
			
		||||
      borderRadius: BorderRadius.circular(8),
 | 
			
		||||
      child: userStatus.when(
 | 
			
		||||
@@ -79,12 +82,13 @@ class AccountStatusCreationWidget extends HookConsumerWidget {
 | 
			
		||||
                (status?.isCustomized ?? false)
 | 
			
		||||
                    ? Padding(
 | 
			
		||||
                      padding: const EdgeInsets.only(left: 4),
 | 
			
		||||
                      child: AccountStatusWidget(uname: uname),
 | 
			
		||||
                      child: AccountStatusWidget(
 | 
			
		||||
                        uname: uname,
 | 
			
		||||
                        padding: renderPadding,
 | 
			
		||||
                      ),
 | 
			
		||||
                    )
 | 
			
		||||
                    : Padding(
 | 
			
		||||
                      padding:
 | 
			
		||||
                          padding ??
 | 
			
		||||
                          EdgeInsets.symmetric(horizontal: 16, vertical: 8),
 | 
			
		||||
                      padding: renderPadding,
 | 
			
		||||
                      child: Column(
 | 
			
		||||
                        crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                        children: [
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,6 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
 | 
			
		||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:island/models/embed.dart';
 | 
			
		||||
@@ -5,7 +8,7 @@ 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 {
 | 
			
		||||
class EmbedLinkWidget extends StatefulWidget {
 | 
			
		||||
  final SnScrappedLink link;
 | 
			
		||||
  final double? maxWidth;
 | 
			
		||||
  final EdgeInsetsGeometry? margin;
 | 
			
		||||
@@ -17,34 +20,117 @@ class EmbedLinkWidget extends StatelessWidget {
 | 
			
		||||
    this.margin,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<EmbedLinkWidget> createState() => _EmbedLinkWidgetState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _EmbedLinkWidgetState extends State<EmbedLinkWidget> {
 | 
			
		||||
  bool? _isSquare;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _checkIfSquare();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _checkIfSquare() async {
 | 
			
		||||
    if (widget.link.imageUrl == null ||
 | 
			
		||||
        widget.link.imageUrl!.isEmpty ||
 | 
			
		||||
        widget.link.imageUrl == widget.link.faviconUrl)
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      final image = CachedNetworkImageProvider(widget.link.imageUrl!);
 | 
			
		||||
      final ImageStream stream = image.resolve(ImageConfiguration.empty);
 | 
			
		||||
      final completer = Completer<ImageInfo>();
 | 
			
		||||
      final listener = ImageStreamListener((
 | 
			
		||||
        ImageInfo info,
 | 
			
		||||
        bool synchronousCall,
 | 
			
		||||
      ) {
 | 
			
		||||
        completer.complete(info);
 | 
			
		||||
      });
 | 
			
		||||
      stream.addListener(listener);
 | 
			
		||||
      final info = await completer.future;
 | 
			
		||||
      stream.removeListener(listener);
 | 
			
		||||
 | 
			
		||||
      final aspectRatio = info.image.width / info.image.height;
 | 
			
		||||
      setState(() {
 | 
			
		||||
        _isSquare = aspectRatio >= 0.9 && aspectRatio <= 1.1;
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      // If error, assume not square
 | 
			
		||||
      setState(() {
 | 
			
		||||
        _isSquare = false;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _launchUrl() async {
 | 
			
		||||
    final uri = Uri.parse(link.url);
 | 
			
		||||
    final uri = Uri.parse(widget.link.url);
 | 
			
		||||
    if (await canLaunchUrl(uri)) {
 | 
			
		||||
      await launchUrl(uri, mode: LaunchMode.externalApplication);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  String _getBaseUrl(String url) {
 | 
			
		||||
    final uri = Uri.parse(url);
 | 
			
		||||
    final port = uri.port;
 | 
			
		||||
    final defaultPort = uri.scheme == 'https' ? 443 : 80;
 | 
			
		||||
    final portString = port != defaultPort ? ':$port' : '';
 | 
			
		||||
    return '${uri.scheme}://${uri.host}$portString';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @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),
 | 
			
		||||
      width: widget.maxWidth,
 | 
			
		||||
      margin: widget.margin ?? const EdgeInsets.symmetric(vertical: 8),
 | 
			
		||||
      child: Card(
 | 
			
		||||
        margin: EdgeInsets.zero,
 | 
			
		||||
        clipBehavior: Clip.antiAlias,
 | 
			
		||||
        child: InkWell(
 | 
			
		||||
          onTap: _launchUrl,
 | 
			
		||||
          child: Row(
 | 
			
		||||
            children: [
 | 
			
		||||
              // Sqaure open graph image
 | 
			
		||||
              if (_isSquare == true) ...[
 | 
			
		||||
                Flexible(
 | 
			
		||||
                  child: ConstrainedBox(
 | 
			
		||||
                    constraints: const BoxConstraints(maxHeight: 120),
 | 
			
		||||
                    child: AspectRatio(
 | 
			
		||||
                      aspectRatio: 1,
 | 
			
		||||
                      child: UniversalImage(
 | 
			
		||||
                        uri: widget.link.imageUrl!,
 | 
			
		||||
                        fit: BoxFit.cover,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                const Gap(8),
 | 
			
		||||
              ],
 | 
			
		||||
              Expanded(
 | 
			
		||||
                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),
 | 
			
		||||
                    if (widget.link.imageUrl != null &&
 | 
			
		||||
                        widget.link.imageUrl!.isNotEmpty &&
 | 
			
		||||
                        widget.link.imageUrl != widget.link.faviconUrl &&
 | 
			
		||||
                        _isSquare != true)
 | 
			
		||||
                      Container(
 | 
			
		||||
                        width: double.infinity,
 | 
			
		||||
                        color:
 | 
			
		||||
                            Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
			
		||||
                        child: IntrinsicHeight(
 | 
			
		||||
                          child: UniversalImage(
 | 
			
		||||
                            uri: widget.link.imageUrl!,
 | 
			
		||||
                            fit: BoxFit.cover,
 | 
			
		||||
                            useFallbackImage: false,
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
 | 
			
		||||
                    // Content
 | 
			
		||||
@@ -56,15 +142,23 @@ class EmbedLinkWidget extends StatelessWidget {
 | 
			
		||||
                          // Site info row
 | 
			
		||||
                          Row(
 | 
			
		||||
                            children: [
 | 
			
		||||
                        // Favicon
 | 
			
		||||
                        if (link.faviconUrl?.isNotEmpty ?? false) ...[
 | 
			
		||||
                              if (widget.link.faviconUrl?.isNotEmpty ??
 | 
			
		||||
                                  false) ...[
 | 
			
		||||
                                ClipRRect(
 | 
			
		||||
                                  borderRadius: BorderRadius.circular(4),
 | 
			
		||||
                                  child: UniversalImage(
 | 
			
		||||
                              uri: link.faviconUrl!,
 | 
			
		||||
                                    uri:
 | 
			
		||||
                                        widget.link.faviconUrl!.startsWith('//')
 | 
			
		||||
                                            ? 'https:${widget.link.faviconUrl!}'
 | 
			
		||||
                                            : widget.link.faviconUrl!
 | 
			
		||||
                                                .startsWith('/')
 | 
			
		||||
                                            ? _getBaseUrl(widget.link.url) +
 | 
			
		||||
                                                widget.link.faviconUrl!
 | 
			
		||||
                                            : widget.link.faviconUrl!,
 | 
			
		||||
                                    width: 16,
 | 
			
		||||
                                    height: 16,
 | 
			
		||||
                                    fit: BoxFit.cover,
 | 
			
		||||
                                    useFallbackImage: false,
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ),
 | 
			
		||||
                                const Gap(8),
 | 
			
		||||
@@ -80,9 +174,9 @@ class EmbedLinkWidget extends StatelessWidget {
 | 
			
		||||
                              // Site name
 | 
			
		||||
                              Expanded(
 | 
			
		||||
                                child: Text(
 | 
			
		||||
                            (link.siteName?.isNotEmpty ?? false)
 | 
			
		||||
                                ? link.siteName!
 | 
			
		||||
                                : Uri.parse(link.url).host,
 | 
			
		||||
                                  (widget.link.siteName?.isNotEmpty ?? false)
 | 
			
		||||
                                      ? widget.link.siteName!
 | 
			
		||||
                                      : Uri.parse(widget.link.url).host,
 | 
			
		||||
                                  style: theme.textTheme.bodySmall?.copyWith(
 | 
			
		||||
                                    color: colorScheme.onSurfaceVariant,
 | 
			
		||||
                                  ),
 | 
			
		||||
@@ -103,35 +197,35 @@ class EmbedLinkWidget extends StatelessWidget {
 | 
			
		||||
                          const Gap(8),
 | 
			
		||||
 | 
			
		||||
                          // Title
 | 
			
		||||
                    if (link.title.isNotEmpty) ...[
 | 
			
		||||
                          if (widget.link.title.isNotEmpty) ...[
 | 
			
		||||
                            Text(
 | 
			
		||||
                        link.title,
 | 
			
		||||
                              widget.link.title,
 | 
			
		||||
                              style: theme.textTheme.titleMedium?.copyWith(
 | 
			
		||||
                                fontWeight: FontWeight.w600,
 | 
			
		||||
                              ),
 | 
			
		||||
                        maxLines: 2,
 | 
			
		||||
                              maxLines: _isSquare == true ? 1 : 2,
 | 
			
		||||
                              overflow: TextOverflow.ellipsis,
 | 
			
		||||
                            ),
 | 
			
		||||
                      const Gap(4),
 | 
			
		||||
                            Gap(_isSquare == true ? 2 : 4),
 | 
			
		||||
                          ],
 | 
			
		||||
 | 
			
		||||
                          // Description
 | 
			
		||||
                    if (link.description != null &&
 | 
			
		||||
                        link.description!.isNotEmpty) ...[
 | 
			
		||||
                          if (widget.link.description != null &&
 | 
			
		||||
                              widget.link.description!.isNotEmpty) ...[
 | 
			
		||||
                            Text(
 | 
			
		||||
                        link.description!,
 | 
			
		||||
                              widget.link.description!,
 | 
			
		||||
                              style: theme.textTheme.bodyMedium?.copyWith(
 | 
			
		||||
                                color: colorScheme.onSurfaceVariant,
 | 
			
		||||
                              ),
 | 
			
		||||
                        maxLines: 3,
 | 
			
		||||
                              maxLines: _isSquare == true ? 1 : 3,
 | 
			
		||||
                              overflow: TextOverflow.ellipsis,
 | 
			
		||||
                            ),
 | 
			
		||||
                      const Gap(8),
 | 
			
		||||
                            Gap(_isSquare == true ? 4 : 8),
 | 
			
		||||
                          ],
 | 
			
		||||
 | 
			
		||||
                          // URL
 | 
			
		||||
                          Text(
 | 
			
		||||
                      link.url,
 | 
			
		||||
                            widget.link.url,
 | 
			
		||||
                            style: theme.textTheme.bodySmall?.copyWith(
 | 
			
		||||
                              color: colorScheme.primary,
 | 
			
		||||
                              decoration: TextDecoration.underline,
 | 
			
		||||
@@ -141,11 +235,12 @@ class EmbedLinkWidget extends StatelessWidget {
 | 
			
		||||
                          ),
 | 
			
		||||
 | 
			
		||||
                          // Author and publish date
 | 
			
		||||
                    if (link.author != null || link.publishedDate != null) ...[
 | 
			
		||||
                          if (widget.link.author != null ||
 | 
			
		||||
                              widget.link.publishedDate != null) ...[
 | 
			
		||||
                            const Gap(8),
 | 
			
		||||
                            Row(
 | 
			
		||||
                              children: [
 | 
			
		||||
                          if (link.author != null) ...[
 | 
			
		||||
                                if (widget.link.author != null) ...[
 | 
			
		||||
                                  Icon(
 | 
			
		||||
                                    Symbols.person,
 | 
			
		||||
                                    size: 14,
 | 
			
		||||
@@ -153,15 +248,16 @@ class EmbedLinkWidget extends StatelessWidget {
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  const Gap(4),
 | 
			
		||||
                                  Text(
 | 
			
		||||
                              link.author!,
 | 
			
		||||
                                    widget.link.author!,
 | 
			
		||||
                                    style: theme.textTheme.bodySmall?.copyWith(
 | 
			
		||||
                                      color: colorScheme.onSurfaceVariant,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ],
 | 
			
		||||
                          if (link.author != null && link.publishedDate != null)
 | 
			
		||||
                                if (widget.link.author != null &&
 | 
			
		||||
                                    widget.link.publishedDate != null)
 | 
			
		||||
                                  const Gap(16),
 | 
			
		||||
                          if (link.publishedDate != null) ...[
 | 
			
		||||
                                if (widget.link.publishedDate != null) ...[
 | 
			
		||||
                                  Icon(
 | 
			
		||||
                                    Symbols.schedule,
 | 
			
		||||
                                    size: 14,
 | 
			
		||||
@@ -169,7 +265,7 @@ class EmbedLinkWidget extends StatelessWidget {
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  const Gap(4),
 | 
			
		||||
                                  Text(
 | 
			
		||||
                              _formatDate(link.publishedDate!),
 | 
			
		||||
                                    _formatDate(widget.link.publishedDate!),
 | 
			
		||||
                                    style: theme.textTheme.bodySmall?.copyWith(
 | 
			
		||||
                                      color: colorScheme.onSurfaceVariant,
 | 
			
		||||
                                    ),
 | 
			
		||||
@@ -184,6 +280,9 @@ class EmbedLinkWidget extends StatelessWidget {
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_blurhash/flutter_blurhash.dart';
 | 
			
		||||
import 'package:flutter_svg/flutter_svg.dart';
 | 
			
		||||
 | 
			
		||||
class UniversalImage extends StatelessWidget {
 | 
			
		||||
  final String uri;
 | 
			
		||||
@@ -9,6 +10,8 @@ class UniversalImage extends StatelessWidget {
 | 
			
		||||
  final double? width;
 | 
			
		||||
  final double? height;
 | 
			
		||||
  final bool noCacheOptimization;
 | 
			
		||||
  final bool isSvg;
 | 
			
		||||
  final bool useFallbackImage;
 | 
			
		||||
 | 
			
		||||
  const UniversalImage({
 | 
			
		||||
    super.key,
 | 
			
		||||
@@ -18,10 +21,26 @@ class UniversalImage extends StatelessWidget {
 | 
			
		||||
    this.width,
 | 
			
		||||
    this.height,
 | 
			
		||||
    this.noCacheOptimization = false,
 | 
			
		||||
    this.isSvg = false,
 | 
			
		||||
    this.useFallbackImage = true,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final isSvgImage = isSvg || uri.toLowerCase().endsWith('.svg');
 | 
			
		||||
 | 
			
		||||
    if (isSvgImage) {
 | 
			
		||||
      return SvgPicture.network(
 | 
			
		||||
        uri,
 | 
			
		||||
        fit: fit,
 | 
			
		||||
        width: width,
 | 
			
		||||
        height: height,
 | 
			
		||||
        placeholderBuilder:
 | 
			
		||||
            (BuildContext context) =>
 | 
			
		||||
                Center(child: CircularProgressIndicator()),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    int? cacheWidth;
 | 
			
		||||
    int? cacheHeight;
 | 
			
		||||
    if (width != null && height != null && !noCacheOptimization) {
 | 
			
		||||
@@ -50,13 +69,15 @@ class UniversalImage extends StatelessWidget {
 | 
			
		||||
                child: CircularProgressIndicator(value: progress.progress),
 | 
			
		||||
              );
 | 
			
		||||
            },
 | 
			
		||||
            errorWidget: (context, url, error) {
 | 
			
		||||
              return Image.asset(
 | 
			
		||||
            errorWidget:
 | 
			
		||||
                (context, url, error) =>
 | 
			
		||||
                    useFallbackImage
 | 
			
		||||
                        ? Image.asset(
 | 
			
		||||
                          'assets/images/media-offline.jpg',
 | 
			
		||||
                          fit: BoxFit.cover,
 | 
			
		||||
                          key: Key('image-broke-$uri'),
 | 
			
		||||
              );
 | 
			
		||||
            },
 | 
			
		||||
                        )
 | 
			
		||||
                        : SizedBox.shrink(),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user