LCOV - code coverage report
Current view: top level - lib/widgets - filebubble.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 228 0.0 %
Date: 2026-07-15 19:28:30 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:io';
       2             : import 'dart:math';
       3             : import 'dart:typed_data';
       4             : 
       5             : import 'package:cwtch/config.dart';
       6             : import 'package:cwtch/cwtch/cwtch.dart';
       7             : import 'package:cwtch/cwtch_icons_icons.dart';
       8             : import 'package:cwtch/models/contact.dart';
       9             : import 'package:cwtch/models/filedownloadprogress.dart';
      10             : import 'package:cwtch/models/message.dart';
      11             : import 'package:cwtch/models/profile.dart';
      12             : import 'package:cwtch/themes/opaque.dart';
      13             : import 'package:cwtch/widgets/malformedbubble.dart';
      14             : import 'package:cwtch/widgets/messageBubbleWidgetHelpers.dart';
      15             : import 'package:file_picker/file_picker.dart';
      16             : import 'package:flutter/material.dart';
      17             : import 'package:provider/provider.dart';
      18             : import '../main.dart';
      19             : 
      20             : import 'package:cwtch/l10n/app_localizations.dart';
      21             : 
      22             : import '../models/redaction.dart';
      23             : import '../settings.dart';
      24             : import 'messagebubbledecorations.dart';
      25             : 
      26             : // Like MessageBubble but for displaying chat overlay 100/101 invitations
      27             : // Offers the user an accept/reject button if they don't have a matching contact already
      28             : class FileBubble extends StatefulWidget {
      29             :   final String nameSuggestion;
      30             :   final String rootHash;
      31             :   final String nonce;
      32             :   final int fileSize;
      33             :   final bool interactive;
      34             :   final bool isAuto;
      35             :   final bool isPreview;
      36             : 
      37           0 :   FileBubble(this.nameSuggestion, this.rootHash, this.nonce, this.fileSize, {this.isAuto = false, this.interactive = true, this.isPreview = false});
      38             : 
      39           0 :   @override
      40           0 :   FileBubbleState createState() => FileBubbleState();
      41             : 
      42           0 :   String fileKey() {
      43           0 :     return this.rootHash + "." + this.nonce;
      44             :   }
      45             : }
      46             : 
      47             : class FileBubbleState extends State<FileBubble> {
      48             :   File? myFile;
      49             : 
      50           0 :   @override
      51             :   void initState() {
      52           0 :     super.initState();
      53             :   }
      54             : 
      55           0 :   Widget getPreview(context) {
      56             :     const constraintSize = 150.0;
      57           0 :     final imageSize = MediaQuery.sizeOf(context);
      58           0 :     final aspectRatio = imageSize.width / imageSize.height;
      59           0 :     final widgetWidth = (constraintSize * MediaQuery.devicePixelRatioOf(context)).round();
      60           0 :     final memCacheWidth = min(widgetWidth, imageSize.width);
      61           0 :     return Container(
      62           0 :       constraints: BoxConstraints(maxHeight: min(constraintSize, max(imageSize.width, imageSize.height))),
      63           0 :       child: Image.file(
      64           0 :         myFile!,
      65             :         // limit the amount of space the image can decode too, we keep this high-ish to allow quality previews...
      66           0 :         cacheWidth: memCacheWidth.round(),
      67             :         filterQuality: FilterQuality.medium,
      68             :         fit: BoxFit.scaleDown,
      69             :         alignment: Alignment.center,
      70             :         isAntiAlias: false,
      71           0 :         errorBuilder: (context, error, stackTrace) {
      72           0 :           return MalformedBubble();
      73             :         },
      74             :       ),
      75             :     );
      76             :   }
      77             : 
      78           0 :   @override
      79             :   Widget build(BuildContext context) {
      80           0 :     var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
      81           0 :     var flagStarted = Provider.of<MessageMetadata>(context).attributes["file-downloaded"] == "true";
      82             :     var borderRadius = 15.0;
      83           0 :     var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment);
      84           0 :     var showImages = Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment);
      85           0 :     DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp;
      86             : 
      87           0 :     var metadata = Provider.of<MessageMetadata>(context);
      88           0 :     var path = Provider.of<ProfileInfoState>(context).downloadFinalPath(widget.fileKey());
      89             : 
      90             :     // If we haven't stored the filepath in message attributes then save it
      91           0 :     if (metadata.attributes["filepath"] != null && metadata.attributes["filepath"].toString().isNotEmpty) {
      92           0 :       path = metadata.attributes["filepath"];
      93           0 :     } else if (path != null && metadata.attributes["filepath"] == null) {
      94           0 :       Provider.of<FlwtchState>(context).cwtch.SetMessageAttribute(metadata.profileOnion, metadata.conversationIdentifier, 0, metadata.messageID, "filepath", path);
      95             :     }
      96             : 
      97             :     // the file is downloaded when it is from the sender AND the path is known OR when we get an explicit downloadComplete
      98           0 :     var downloadComplete = (fromMe && path != null) || Provider.of<ProfileInfoState>(context).downloadComplete(widget.fileKey());
      99           0 :     var downloadInterrupted = Provider.of<ProfileInfoState>(context).downloadInterrupted(widget.fileKey());
     100             : 
     101             :     var isImagePreview = false;
     102             :     if (path != null) {
     103           0 :       isImagePreview = Provider.of<Settings>(context).isImage(path);
     104             :     }
     105             : 
     106             :     if (downloadComplete && path != null) {
     107             :       if (isImagePreview) {
     108           0 :         if (myFile == null || myFile?.path != path) {
     109           0 :           myFile = new File(path);
     110             :           // reset
     111           0 :           if (myFile?.existsSync() == false) {
     112           0 :             myFile = null;
     113             :             //Provider.of<ProfileInfoState>(context, listen: false).downloadReset(widget.fileKey());
     114           0 :             Provider.of<MessageMetadata>(context, listen: false).attributes["filepath"] = null;
     115           0 :             Provider.of<MessageMetadata>(context, listen: false).attributes["file-downloaded"] = "false";
     116           0 :             Provider.of<MessageMetadata>(context, listen: false).attributes["file-missing"] = "true";
     117           0 :             Provider.of<FlwtchState>(context, listen: false).cwtch.SetMessageAttribute(metadata.profileOnion, metadata.conversationIdentifier, 0, metadata.messageID, "file-downloaded", "false");
     118           0 :             Provider.of<FlwtchState>(context, listen: false).cwtch.SetMessageAttribute(metadata.profileOnion, metadata.conversationIdentifier, 0, metadata.messageID, "filepath", "");
     119           0 :             Provider.of<FlwtchState>(context, listen: false).cwtch.SetMessageAttribute(metadata.profileOnion, metadata.conversationIdentifier, 0, metadata.messageID, "file-missing", "true");
     120             :           } else {
     121           0 :             Provider.of<MessageMetadata>(context, listen: false).attributes["file-missing"] = "false";
     122           0 :             Provider.of<FlwtchState>(context, listen: false).cwtch.SetMessageAttribute(metadata.profileOnion, metadata.conversationIdentifier, 0, metadata.messageID, "file-missing", "false");
     123           0 :             setState(() {});
     124             :           }
     125             :         }
     126             :       }
     127             :     }
     128             : 
     129           0 :     var downloadActive = Provider.of<ProfileInfoState>(context).downloadActive(widget.fileKey());
     130           0 :     var downloadGotManifest = Provider.of<ProfileInfoState>(context).downloadGotManifest(widget.fileKey());
     131             : 
     132           0 :     var messageStatusWidget = MessageBubbleDecoration(ackd: metadata.ackd, errored: metadata.error, messageDate: messageDate, fromMe: fromMe);
     133             : 
     134             :     // If the sender is not us, then we want to give them a nickname...
     135             :     var senderDisplayStr = "";
     136             :     var senderIsContact = false;
     137             :     if (!fromMe) {
     138           0 :       ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
     139             :       if (contact != null) {
     140           0 :         senderDisplayStr = redactedNick(context, contact.onion, contact.nickname);
     141             :         senderIsContact = true;
     142             :       } else {
     143           0 :         senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
     144             :       }
     145             :     }
     146             : 
     147             :     // if we should show a preview i.e. we are in a quote bubble
     148             :     // then do that here...
     149           0 :     if (showImages && isImagePreview && widget.isPreview && myFile != null) {
     150             :       // if the image exists then just show the image as a preview
     151           0 :       return getPreview(context);
     152           0 :     } else if (showFileSharing && widget.isPreview) {
     153             :       // otherwise just show a summary...
     154           0 :       return Row(
     155           0 :         children: [
     156           0 :           Icon(CwtchIcons.attached_file_3, size: 32, color: Provider.of<Settings>(context).theme.messageFromMeTextColor),
     157           0 :           Flexible(
     158           0 :             child: Text(
     159           0 :               widget.nameSuggestion,
     160           0 :               style: TextStyle(fontWeight: FontWeight.bold, fontFamily: "Inter", color: Provider.of<Settings>(context).theme.messageFromMeTextColor),
     161             :             ),
     162             :           ),
     163             :         ],
     164             :       );
     165             :     }
     166             : 
     167           0 :     var wdgSender = Visibility(
     168           0 :       visible: widget.interactive,
     169           0 :       child: Container(
     170           0 :         height: 14 * Provider.of<Settings>(context).fontScaling,
     171             :         clipBehavior: Clip.hardEdge,
     172           0 :         decoration: BoxDecoration(),
     173           0 :         child: compileSenderWidget(context, null, fromMe, senderDisplayStr),
     174             :       ),
     175             :     );
     176             :     var isPreview = false;
     177             :     var wdgMessage = !showFileSharing
     178           0 :         ? Text(AppLocalizations.of(context)!.messageEnableFileSharing, style: Provider.of<Settings>(context).scaleFonts(defaultTextStyle))
     179             :         : fromMe
     180           0 :         ? senderFileChrome(AppLocalizations.of(context)!.messageFileSent, widget.nameSuggestion, widget.rootHash)
     181           0 :         : (fileChrome(
     182           0 :             AppLocalizations.of(context)!.messageFileOffered + ":",
     183           0 :             widget.nameSuggestion,
     184           0 :             widget.rootHash,
     185           0 :             widget.fileSize,
     186           0 :             Provider.of<ProfileInfoState>(context).downloadSpeed(widget.fileKey()),
     187             :           ));
     188             :     Widget wdgDecorations;
     189             : 
     190             :     if (!showFileSharing) {
     191           0 :       wdgDecorations = Text('\u202F');
     192             :     } else if ((fromMe || downloadComplete) && path != null) {
     193             :       // in this case, whatever marked download.complete would have also set the path
     194           0 :       if (myFile != null && Provider.of<Settings>(context).shouldPreview(path)) {
     195             :         isPreview = true;
     196           0 :         wdgDecorations = Center(
     197             :           widthFactor: 1.0,
     198           0 :           child: MouseRegion(
     199             :             cursor: SystemMouseCursors.click,
     200           0 :             child: GestureDetector(
     201           0 :               child: Padding(padding: EdgeInsets.all(1.0), child: getPreview(context)),
     202           0 :               onTap: () {
     203           0 :                 pop(context, myFile!, widget.nameSuggestion);
     204             :               },
     205             :             ),
     206             :           ),
     207             :         );
     208             :       } else if (fromMe) {
     209           0 :         wdgDecorations = Text('\u202F');
     210             :       } else {
     211           0 :         wdgDecorations = Visibility(
     212           0 :           visible: widget.interactive,
     213           0 :           child: SelectableText(AppLocalizations.of(context)!.fileSavedTo + ': ' + path + '\u202F', style: Provider.of<Settings>(context).scaleFonts(defaultTextStyle)),
     214             :         );
     215             :       }
     216             :     } else if (downloadActive) {
     217             :       if (!downloadGotManifest) {
     218           0 :         wdgDecorations = Visibility(
     219           0 :           visible: widget.interactive,
     220           0 :           child: SelectableText(AppLocalizations.of(context)!.retrievingManifestMessage + '\u202F', style: Provider.of<Settings>(context).scaleFonts(defaultTextStyle)),
     221             :         );
     222             :       } else {
     223           0 :         wdgDecorations = Visibility(
     224           0 :           visible: widget.interactive,
     225           0 :           child: LinearProgressIndicator(value: Provider.of<ProfileInfoState>(context).downloadProgress(widget.fileKey()), color: Provider.of<Settings>(context).theme.defaultButtonActiveColor),
     226             :         );
     227             :       }
     228             :     } else if (flagStarted) {
     229             :       // in this case, the download was done in a previous application launch,
     230             :       // so we probably have to request an info lookup
     231             :       if (!downloadInterrupted) {
     232           0 :         wdgDecorations = Text(AppLocalizations.of(context)!.fileCheckingStatus + '...' + '\u202F', style: Provider.of<Settings>(context).scaleFonts(defaultTextStyle));
     233             :         // We should have already requested this...
     234             :       } else {
     235           0 :         var path = Provider.of<ProfileInfoState>(context).downloadFinalPath(widget.fileKey()) ?? "";
     236           0 :         wdgDecorations = Visibility(
     237           0 :           visible: widget.interactive,
     238           0 :           child: Column(
     239             :             crossAxisAlignment: CrossAxisAlignment.start,
     240           0 :             children: [
     241           0 :               Text(AppLocalizations.of(context)!.fileInterrupted + ': ' + path + '\u202F', style: Provider.of<Settings>(context).scaleFonts(defaultTextStyle)),
     242           0 :               ElevatedButton(
     243           0 :                 onPressed: _btnResume,
     244           0 :                 child: Text(AppLocalizations.of(context)!.verfiyResumeButton, style: Provider.of<Settings>(context).scaleFonts(defaultTextButtonStyle)),
     245             :               ),
     246             :             ],
     247             :           ),
     248             :         );
     249             :       }
     250             :     } else if (!senderIsContact) {
     251           0 :       wdgDecorations = Text(AppLocalizations.of(context)!.msgAddToAccept, style: Provider.of<Settings>(context).scaleFonts(defaultTextStyle));
     252           0 :     } else if (!widget.isAuto || Provider.of<MessageMetadata>(context).attributes["file-missing"] == "false") {
     253             :       //Note: we need this second case to account for scenarios where a user deletes the downloaded file, we won't automatically
     254             :       // fetch it again, so we need to offer the user the ability to restart..
     255           0 :       wdgDecorations = Visibility(
     256           0 :         visible: widget.interactive,
     257           0 :         child: Center(
     258             :           widthFactor: 1,
     259           0 :           child: Wrap(
     260           0 :             children: [
     261           0 :               Padding(
     262           0 :                 padding: EdgeInsets.all(5),
     263           0 :                 child: ElevatedButton(
     264           0 :                   child: Text(AppLocalizations.of(context)!.downloadFileButton + '\u202F', style: Provider.of<Settings>(context).scaleFonts(defaultTextButtonStyle)),
     265           0 :                   onPressed: _btnAccept,
     266             :                 ),
     267             :               ),
     268             :             ],
     269             :           ),
     270             :         ),
     271             :       );
     272             :     } else {
     273           0 :       wdgDecorations = Container();
     274             :     }
     275             : 
     276           0 :     return Container(
     277           0 :       constraints: BoxConstraints(maxWidth: MediaQuery.sizeOf(context).width * 0.3),
     278           0 :       decoration: BoxDecoration(
     279           0 :         color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor,
     280           0 :         border: Border.all(color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor, width: 1),
     281           0 :         borderRadius: BorderRadius.only(
     282           0 :           topLeft: Radius.circular(borderRadius),
     283           0 :           topRight: Radius.circular(borderRadius),
     284           0 :           bottomLeft: fromMe ? Radius.circular(borderRadius) : Radius.zero,
     285           0 :           bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadius),
     286             :         ),
     287             :       ),
     288           0 :       child: Theme(
     289           0 :         data: Theme.of(context).copyWith(
     290           0 :           textSelectionTheme: TextSelectionThemeData(
     291           0 :             cursorColor: Provider.of<Settings>(context).theme.messageSelectionColor,
     292           0 :             selectionColor: Provider.of<Settings>(context).theme.messageSelectionColor,
     293           0 :             selectionHandleColor: Provider.of<Settings>(context).theme.messageSelectionColor,
     294             :           ),
     295             : 
     296             :           // Horrifying Hack: Flutter doesn't give us direct control over system menus but instead picks BG color from TextButtonThemeData ¯\_(ツ)_/¯
     297           0 :           textButtonTheme: TextButtonThemeData(style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Provider.of<Settings>(context).theme.menuBackgroundColor))),
     298             :         ),
     299           0 :         child: Padding(
     300           0 :           padding: EdgeInsets.all(9.0),
     301           0 :           child: Column(
     302             :             crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
     303             :             mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start,
     304             :             mainAxisSize: MainAxisSize.min,
     305           0 :             children: [
     306             :               wdgSender,
     307           0 :               isPreview ? Container(width: 0, padding: EdgeInsets.zero, margin: EdgeInsets.zero) : wdgMessage,
     308             :               wdgDecorations,
     309             :               messageStatusWidget,
     310             :             ],
     311             :           ),
     312             :         ),
     313             :       ),
     314             :     );
     315             :   }
     316             : 
     317           0 :   void _btnAccept() async {
     318             :     String? selectedFileName;
     319             :     File? file;
     320           0 :     var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
     321           0 :     var conversation = Provider.of<ContactInfoState>(context, listen: false).identifier;
     322           0 :     var idx = Provider.of<MessageMetadata>(context, listen: false).messageID;
     323             : 
     324           0 :     if (Platform.isAndroid) {
     325           0 :       Provider.of<ProfileInfoState>(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil());
     326           0 :       Provider.of<FlwtchState>(context, listen: false).cwtch.SetMessageAttribute(profileOnion, conversation, 0, idx, "file-downloaded", "true");
     327           0 :       ContactInfoState? contact = Provider.of<ProfileInfoState>(context, listen: false).contactList.findContact(Provider.of<MessageMetadata>(context, listen: false).senderHandle);
     328             :       if (contact != null) {
     329           0 :         var manifestPath = Provider.of<Settings>(context, listen: false).downloadPath + "/" + widget.fileKey() + ".manifest";
     330             : 
     331           0 :         Provider.of<FlwtchState>(context, listen: false).cwtch.CreateDownloadableFile(profileOnion, contact.identifier, widget.nameSuggestion, widget.fileKey(), manifestPath);
     332             :       }
     333             :     } else {
     334             :       try {
     335           0 :         var downloadPath = Provider.of<Settings>(context, listen: false).downloadPath;
     336           0 :         if (downloadPath == "") {
     337           0 :           downloadPath = Provider.of<FlwtchState>(context, listen: false).cwtch.defaultDownloadPath() ?? "";
     338             :         }
     339             : 
     340           0 :         selectedFileName = await FilePicker.saveFile(fileName: widget.nameSuggestion, bytes: Uint8List(0), initialDirectory: downloadPath, lockParentWindow: true);
     341             :         if (selectedFileName != null) {
     342           0 :           file = File(selectedFileName);
     343           0 :           EnvironmentConfig.debugLog("saving to " + file.path);
     344           0 :           var manifestPath = file.path + ".manifest";
     345           0 :           Provider.of<ProfileInfoState>(context, listen: false).downloadInit(widget.fileKey(), (widget.fileSize / 4096).ceil());
     346           0 :           Provider.of<FlwtchState>(context, listen: false).cwtch.SetMessageAttribute(profileOnion, conversation, 0, idx, "file-downloaded", "true");
     347           0 :           ContactInfoState? contact = Provider.of<ProfileInfoState>(context, listen: false).contactList.findContact(Provider.of<MessageMetadata>(context, listen: false).senderHandle);
     348             :           if (contact != null) {
     349           0 :             Provider.of<FlwtchState>(context, listen: false).cwtch.DownloadFile(profileOnion, contact.identifier, file.path, manifestPath, widget.fileKey());
     350             :           }
     351             :         }
     352             :       } catch (e) {
     353           0 :         print(e);
     354             :       }
     355             :     }
     356             :   }
     357             : 
     358           0 :   void _btnResume() async {
     359           0 :     var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
     360           0 :     var handle = Provider.of<MessageMetadata>(context, listen: false).conversationIdentifier;
     361           0 :     Provider.of<ProfileInfoState>(context, listen: false).downloadMarkResumed(widget.fileKey());
     362           0 :     Provider.of<FlwtchState>(context, listen: false).cwtch.VerifyOrResumeDownload(profileOnion, handle, widget.fileKey());
     363             :   }
     364             : 
     365             :   // Construct an file chrome for the sender
     366           0 :   Widget senderFileChrome(String chrome, String fileName, String rootHash) {
     367           0 :     var settings = Provider.of<Settings>(context);
     368           0 :     return ListTile(
     369             :       visualDensity: VisualDensity.compact,
     370           0 :       contentPadding: EdgeInsets.all(1.0),
     371           0 :       title: SelectableText(
     372           0 :         fileName + '\u202F',
     373           0 :         style: settings.scaleFonts(defaultMessageTextStyle.copyWith(overflow: TextOverflow.ellipsis, fontWeight: FontWeight.bold, color: Provider.of<Settings>(context).theme.messageFromMeTextColor)),
     374             :         textAlign: TextAlign.left,
     375             :         textWidthBasis: TextWidthBasis.longestLine,
     376             :         maxLines: 2,
     377             :       ),
     378           0 :       subtitle: SelectableText(
     379           0 :         prettyBytes(widget.fileSize) + '\u202F' + '\n' + 'sha512: ' + rootHash + '\u202F',
     380           0 :         style: settings.scaleFonts(defaultSmallTextStyle.copyWith(fontFamily: "RobotoMono", color: Provider.of<Settings>(context).theme.messageFromMeTextColor)),
     381             :         textAlign: TextAlign.left,
     382             :         maxLines: 4,
     383             :         textWidthBasis: TextWidthBasis.longestLine,
     384             :       ),
     385           0 :       leading: FittedBox(child: Icon(CwtchIcons.attached_file_3, size: 32, color: Provider.of<Settings>(context).theme.messageFromOtherTextColor)),
     386             :     );
     387             :   }
     388             : 
     389             :   // Construct an file chrome
     390           0 :   Widget fileChrome(String chrome, String fileName, String rootHash, int fileSize, String speed) {
     391           0 :     var settings = Provider.of<Settings>(context);
     392           0 :     return ListTile(
     393             :       visualDensity: VisualDensity.compact,
     394           0 :       contentPadding: EdgeInsets.all(1.0),
     395           0 :       title: SelectableText(
     396           0 :         fileName + '\u202F',
     397           0 :         style: settings.scaleFonts(
     398           0 :           defaultMessageTextStyle.copyWith(overflow: TextOverflow.ellipsis, fontWeight: FontWeight.bold, color: Provider.of<Settings>(context).theme.messageFromOtherTextColor),
     399             :         ),
     400             :         textAlign: TextAlign.left,
     401             :         textWidthBasis: TextWidthBasis.longestLine,
     402             :         maxLines: 2,
     403             :       ),
     404           0 :       subtitle: SelectableText(
     405           0 :         prettyBytes(widget.fileSize) + '\u202F' + '\n' + 'sha512: ' + rootHash + '\u202F',
     406           0 :         style: settings.scaleFonts(defaultSmallTextStyle.copyWith(fontFamily: "RobotoMono", color: Provider.of<Settings>(context).theme.messageFromOtherTextColor)),
     407             :         textAlign: TextAlign.left,
     408             :         maxLines: 4,
     409             :         textWidthBasis: TextWidthBasis.longestLine,
     410             :       ),
     411           0 :       leading: FittedBox(child: Icon(CwtchIcons.attached_file_3, size: 32, color: Provider.of<Settings>(context).theme.messageFromOtherTextColor)),
     412             :       // Note: not using Visible here because we want to shrink this to nothing when not in use...
     413           0 :       trailing: speed == "0 B/s"
     414             :           ? null
     415           0 :           : SelectableText(
     416           0 :               speed + '\u202F',
     417           0 :               style: settings.scaleFonts(defaultSmallTextStyle.copyWith(color: Provider.of<Settings>(context).theme.messageFromOtherTextColor)),
     418             :               textAlign: TextAlign.left,
     419             :               maxLines: 1,
     420             :               textWidthBasis: TextWidthBasis.longestLine,
     421             :             ),
     422             :     );
     423             :   }
     424             : 
     425           0 :   void pop(context, File myFile, String meta) async {
     426           0 :     await showDialog(
     427             :       context: context,
     428           0 :       builder: (bcontext) => Dialog(
     429             :         alignment: Alignment.topCenter,
     430           0 :         child: SingleChildScrollView(
     431           0 :           controller: ScrollController(),
     432           0 :           child: Container(
     433           0 :             padding: EdgeInsets.all(10),
     434           0 :             child: Column(
     435             :               mainAxisAlignment: MainAxisAlignment.start,
     436             :               crossAxisAlignment: CrossAxisAlignment.center,
     437           0 :               children: [
     438           0 :                 ListTile(
     439           0 :                   leading: Icon(CwtchIcons.attached_file_3),
     440           0 :                   title: Text(meta),
     441           0 :                   trailing: IconButton(
     442           0 :                     icon: Icon(Icons.close),
     443           0 :                     color: Provider.of<Settings>(bcontext, listen: false).theme.mainTextColor,
     444             :                     iconSize: 32,
     445           0 :                     onPressed: () {
     446           0 :                       Navigator.pop(bcontext, true);
     447             :                     },
     448             :                   ),
     449             :                 ),
     450           0 :                 Padding(
     451           0 :                   padding: EdgeInsets.all(10),
     452           0 :                   child: Image.file(
     453             :                     myFile,
     454           0 :                     cacheWidth: (MediaQuery.of(bcontext).size.width * 0.6).floor(),
     455           0 :                     width: (MediaQuery.of(bcontext).size.width * 0.6),
     456           0 :                     height: (MediaQuery.of(bcontext).size.height * 0.6),
     457             :                     fit: BoxFit.scaleDown,
     458             :                   ),
     459             :                 ),
     460           0 :                 Visibility(
     461           0 :                   visible: !Platform.isAndroid,
     462             :                   maintainSize: false,
     463           0 :                   child: Text(myFile.path, textAlign: TextAlign.center),
     464             :                 ),
     465           0 :                 Visibility(
     466           0 :                   visible: Platform.isAndroid,
     467             :                   maintainSize: false,
     468           0 :                   child: Padding(
     469           0 :                     padding: EdgeInsets.all(10),
     470           0 :                     child: ElevatedButton.icon(icon: Icon(Icons.arrow_downward), onPressed: androidExport, label: Text(AppLocalizations.of(bcontext)!.saveBtn)),
     471             :                   ),
     472             :                 ),
     473             :               ],
     474             :             ),
     475             :           ),
     476             :         ),
     477             :       ),
     478             :     );
     479             :   }
     480             : 
     481           0 :   void androidExport() async {
     482           0 :     if (myFile != null) {
     483           0 :       Provider.of<FlwtchState>(context, listen: false).cwtch.ExportPreviewedFile(myFile!.path, widget.nameSuggestion);
     484             :     }
     485             :   }
     486             : }

Generated by: LCOV version 1.14