@@ -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,157 @@ 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 zulipLocalizations = ZulipLocalizations.of(context);
106+ // return IconButton(
107+ // tooltip: zulipLocalizations.lightboxDownloadImageTooltip,
108+ // icon: const Icon(Icons.download),
109+ // onPressed: () async {
110+ // final scaffoldMessenger = ScaffoldMessenger.of(context);
111+ // String message = zulipLocalizations.lightboxDownloadImageFailed;
112+
113+ // try {
114+ // // Fetch the image with a timeout
115+ // final response = await http.get(url).timeout(
116+ // const Duration(seconds: 30),
117+ // onTimeout: () {
118+ // throw TimeoutException("timed out");
119+ // },
120+ // );
121+ // if (response.statusCode == 200) {
122+ // // Get the external storage directory
123+ // final directory = await getExternalStorageDirectory();
124+ // if (directory == null) {
125+ // message = zulipLocalizations.lightboxDownloadImageError;
126+ // } else {
127+ // final downloadPath = '${directory.path.split("Android")[0]}Download';
128+
129+ // // Create the Downloads folder if it doesn't exist
130+ // final downloadFolder = Directory(downloadPath);
131+ // if (!await downloadFolder.exists()) {
132+ // await downloadFolder.create(recursive: true);
133+ // }
134+
135+ // final fileName = url.pathSegments.last;
136+ // final filePath = '$downloadPath/$fileName';
137+
138+ // final file = File(filePath);
139+ // await file.writeAsBytes(response.bodyBytes);
140+
141+ // // Trigger Media Scanner so it reflects in the gallery.
142+ // await platform.invokeMethod('scanFile', {'path': filePath});
143+
144+ // message = zulipLocalizations.lightboxDownloadImageSuccess;
145+ // }
146+ // } else {
147+ // message = zulipLocalizations.lightboxDownloadImageFailed;
148+ // }
149+ // } catch (e) {
150+ // if (e is TimeoutException || e is SocketException) {
151+ // message = zulipLocalizations.lightboxDownloadImageError;
152+ // } else {
153+ // message = zulipLocalizations.lightboxDownloadImageError;
154+ // }
155+ // }
156+
157+ // // Show a SnackBar notification
158+
159+ // scaffoldMessenger.showSnackBar(
160+ // SnackBar(behavior: SnackBarBehavior.floating, content: Text(message)),
161+ // );
162+ // }
163+ // );
164+ // }
165+ // }
166+
167+ class _DownloadImageButton extends StatelessWidget {
168+ const _DownloadImageButton ({required this .url});
169+
170+ final Uri url;
171+
172+ static const platform = MethodChannel ('gallery_saver' );
173+
174+ @override
175+ Widget build (BuildContext context) {
176+ final zulipLocalizations = ZulipLocalizations .of (context);
177+ return IconButton (
178+ tooltip: zulipLocalizations.lightboxDownloadImageTooltip,
179+ icon: const Icon (Icons .download),
180+ onPressed: () async {
181+ final scaffoldMessenger = ScaffoldMessenger .of (context);
182+ String message = zulipLocalizations.lightboxDownloadImageFailed;
183+
184+ try {
185+ // Fetch the image with a timeout
186+ final response = await http.get (url).timeout (
187+ const Duration (seconds: 30 ),
188+ onTimeout: () {
189+ throw TimeoutException ("timed out" );
190+ },
191+ );
192+
193+ if (response.statusCode == 200 ) {
194+ // Get the external storage directory
195+ final directory = await getExternalStorageDirectory ();
196+ if (directory == null ) {
197+ message = zulipLocalizations.lightboxDownloadImageError;
198+ } else {
199+ // Refactored to use MediaStore for Android 10+ (Scoped Storage)
200+ if (Platform .isAndroid) {
201+ final downloadFolder = await getDownloadDirectory ();
202+ final fileName = url.pathSegments.last;
203+ final filePath = '$downloadFolder /$fileName ' ;
204+
205+ final file = File (filePath);
206+ await file.writeAsBytes (response.bodyBytes);
207+
208+ // Trigger Media Scanner so it reflects in the gallery.
209+ await platform.invokeMethod ('scanFile' , {'path' : filePath});
210+
211+ message = zulipLocalizations.lightboxDownloadImageSuccess;
212+ } else {
213+ message = zulipLocalizations.lightboxDownloadImageError;
214+ }
215+ }
216+ } else {
217+ message = zulipLocalizations.lightboxDownloadImageFailed;
218+ }
219+ } catch (e) {
220+ if (e is TimeoutException || e is SocketException ) {
221+ message = zulipLocalizations.lightboxDownloadImageError;
222+ } else {
223+ message = zulipLocalizations.lightboxDownloadImageError;
224+ }
225+ }
226+
227+ // Show a SnackBar notification
228+ scaffoldMessenger.showSnackBar (
229+ SnackBar (behavior: SnackBarBehavior .floating, content: Text (message)),
230+ );
231+ }
232+ );
233+ }
234+
235+ // Returns the download directory for Android 10+ using scoped storage
236+ Future <String > getDownloadDirectory () async {
237+ if (Platform .isAndroid) {
238+ final directory = await getExternalStorageDirectory ();
239+ final downloadFolder = '${directory ?.path .split ("Android" )[0 ]}Download' ;
240+ return downloadFolder;
241+ }
242+ return '' ;
243+ }
244+ }
245+
246+
92247class _LightboxPageLayout extends StatefulWidget {
93248 const _LightboxPageLayout ({
94249 required this .routeEntranceAnimation,
@@ -258,6 +413,7 @@ class _ImageLightboxPageState extends State<_ImageLightboxPage> {
258413 elevation: elevation,
259414 child: Row (children: [
260415 _CopyLinkButton (url: widget.src),
416+ _DownloadImageButton (url: widget.src)
261417 // TODO(#43): Share image
262418 // TODO(#42): Download image
263419 ]),
0 commit comments