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 : }
|