@@ -3,6 +3,10 @@ import 'package:flutter/scheduler.dart';
33import  'package:flutter/services.dart' ;
44import  'package:intl/intl.dart' ;
55import  'package:video_player/video_player.dart' ;
6+ import  'package:http/http.dart'  as  http;
7+ import  'package:path_provider/path_provider.dart' ;
8+ import  'dart:io' ;
9+ import  'dart:async' ;
610
711import  '../api/core.dart' ;
812import  '../api/model/model.dart' ;
@@ -89,6 +93,95 @@ class _CopyLinkButton extends StatelessWidget {
8993  }
9094}
9195
96+ class  _DownloadImageButton  extends  StatelessWidget  {
97+   const  _DownloadImageButton ({required  this .url});
98+ 
99+   final  Uri  url;
100+ 
101+   static  const  platform =  MethodChannel ('gallery_saver' );
102+ 
103+   @override 
104+   Widget  build (BuildContext  context) {
105+     final  store =  PerAccountStoreWidget .of (context);
106+     final  zulipLocalizations =  ZulipLocalizations .of (context);
107+     return  IconButton (
108+       tooltip:  zulipLocalizations.lightboxDownloadImageTooltip,
109+       icon:  const  Icon (Icons .download),
110+       onPressed:  () async  {
111+         final  scaffoldMessenger =  ScaffoldMessenger .of (context);
112+         String  message =  zulipLocalizations.lightboxDownloadImageFailed;
113+         try  {
114+           // Fetch the image with a timeout 
115+           final  response =  await  http.get (
116+             url,
117+             headers:  {
118+                 if  (url.origin ==  store.account.realmUrl.origin) ...authHeader (
119+                   email:  store.account.email,
120+                   apiKey:  store.account.apiKey,
121+                 ),
122+                 ...userAgentHeader ()
123+               }
124+             ).timeout (
125+             const  Duration (seconds:  30 ),
126+             onTimeout:  () {
127+               throw  TimeoutException ("timed out" );
128+             },
129+           );
130+ 
131+           if  (response.statusCode ==  200 ) {
132+             // Get the external storage directory 
133+             final  directory =  await  getExternalStorageDirectory ();
134+             if  (directory ==  null ) {
135+               message =  zulipLocalizations.lightboxDownloadImageError;
136+             } else  {
137+               // Refactored to use MediaStore for Android 10+ (Scoped Storage) 
138+               if  (Platform .isAndroid) {
139+                 final  downloadFolder =  await  getDownloadDirectory ();
140+                 final  fileName =  url.pathSegments.last;
141+                 final  filePath =  '$downloadFolder /$fileName ' ;
142+ 
143+                 final  file =  File (filePath);
144+                 await  file.writeAsBytes (response.bodyBytes);
145+ 
146+                 // Trigger Media Scanner so it reflects in the gallery. 
147+                 await  platform.invokeMethod ('scanFile' , {'path' :  filePath});
148+ 
149+                 message =  zulipLocalizations.lightboxDownloadImageSuccess;
150+               } else  {
151+                 message =  zulipLocalizations.lightboxDownloadImageError;
152+               }
153+             }
154+           } else  {
155+             message =  zulipLocalizations.lightboxDownloadImageFailed;
156+           }
157+         } catch  (e) {
158+           if  (e is  TimeoutException  ||  e is  SocketException ) {
159+             message =  zulipLocalizations.lightboxDownloadImageError;
160+           } else  {
161+             message =  zulipLocalizations.lightboxDownloadImageError;
162+           }
163+         }
164+ 
165+         // Show a SnackBar notification 
166+         scaffoldMessenger.showSnackBar (
167+           SnackBar (behavior:  SnackBarBehavior .floating, content:  Text (message)),
168+         );
169+       }
170+     );
171+   }
172+ 
173+   // Returns the download directory for Android 10+ using scoped storage 
174+   Future <String > getDownloadDirectory () async  {
175+     if  (Platform .isAndroid) {
176+       final  directory =  await  getExternalStorageDirectory ();
177+       final  downloadFolder =  '${directory ?.path .split ("Android" )[0 ]}Download' ;
178+       return  downloadFolder;
179+     }
180+     return  '' ;
181+   }
182+ }
183+ 
184+ 
92185class  _LightboxPageLayout  extends  StatefulWidget  {
93186  const  _LightboxPageLayout ({
94187    required  this .routeEntranceAnimation,
@@ -258,6 +351,7 @@ class _ImageLightboxPageState extends State<_ImageLightboxPage> {
258351      elevation:  elevation,
259352      child:  Row (children:  [
260353        _CopyLinkButton (url:  widget.src),
354+         _DownloadImageButton (url:  widget.src)
261355        // TODO(#43): Share image 
262356        // TODO(#42): Download image 
263357      ]),
0 commit comments