LCOV - code coverage report
Current view: top level - lib/widgets - filebubble.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 208 0.0 %
Date: 2024-08-22 18:05:30 Functions: 0 0 -

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

Generated by: LCOV version 1.14