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

Generated by: LCOV version 1.14