diff --git a/android/settings.gradle b/android/settings.gradle index 2953f1c..6a0d66d 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,7 +19,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "8.6.0" apply false - id "org.jetbrains.kotlin.android" version "1.8.22" apply false + id "org.jetbrains.kotlin.android" version "2.2.21" apply false id("com.google.gms.google-services") version "4.4.2" apply false } diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 743c8e1..f68ad6f 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -1,9 +1,9 @@ class ApiEndpoints { - static const String baseUrl = "https://stageapi.marcoaiot.com/api"; + // static const String baseUrl = "https://stageapi.marcoaiot.com/api"; // static const String baseUrl = "https://api.marcoaiot.com/api"; // static const String baseUrl = "https://devapi.marcoaiot.com/api"; // static const String baseUrl = "https://mapi.marcoaiot.com/api"; - // static const String baseUrl = "https://api.onfieldwork.com/api"; + static const String baseUrl = "https://api.onfieldwork.com/api"; static const String getMasterCurrencies = "/Master/currencies/list"; diff --git a/lib/helpers/widgets/image_viewer_dialog.dart b/lib/helpers/widgets/image_viewer_dialog.dart index fadfdcc..a5fbe14 100644 --- a/lib/helpers/widgets/image_viewer_dialog.dart +++ b/lib/helpers/widgets/image_viewer_dialog.dart @@ -1,14 +1,27 @@ import 'dart:io'; +import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:gallery_saver_plus/gallery_saver.dart'; +import 'package:http/http.dart' as http; +import 'package:path_provider/path_provider.dart'; +import 'package:get/get.dart'; +import 'package:on_field_work/helpers/widgets/my_confirmation_dialog.dart'; class ImageViewerDialog extends StatefulWidget { final List imageSources; final int initialIndex; + final List? captions; + final String? title; const ImageViewerDialog({ Key? key, required this.imageSources, required this.initialIndex, + this.captions, + this.title, }) : super(key: key); @override @@ -28,93 +41,303 @@ class _ImageViewerDialogState extends State { _controller = PageController(initialPage: widget.initialIndex); } + Future shareImage(dynamic image) async { + try { + if (isFile(image)) { + await Share.shareXFiles([XFile(image.path)], + text: 'Check out this image!'); + } else if (image is String) { + await Share.share(image, subject: 'Check out this image!'); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to share image: $e')), + ); + } + } + + Future downloadImage(dynamic image) async { + try { + if (isFile(image)) { + await GallerySaver.saveImage(image.path); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Image saved to gallery')), + ); + } else if (image is String) { + final response = await http.get(Uri.parse(image)); + final bytes = response.bodyBytes; + + final tempDir = await getTemporaryDirectory(); + final filePath = + '${tempDir.path}/${DateTime.now().millisecondsSinceEpoch}.png'; + final file = await File(filePath).writeAsBytes(bytes); + + await GallerySaver.saveImage(file.path); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Image saved to gallery')), + ); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to save image: $e')), + ); + } + } + @override Widget build(BuildContext context) { - final double dialogHeight = MediaQuery.of(context).size.height * 0.55; - - return Dialog( - backgroundColor: Colors.transparent, - insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 100), - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: Container( - height: dialogHeight, - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - blurRadius: 12, - offset: const Offset(0, 4), + return Scaffold( + backgroundColor: Colors.black, + body: Stack( + children: [ + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF101018), Color(0xFF050509)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, ), - ], + ), ), - child: Column( - children: [ - // Top Close Button - Align( - alignment: Alignment.topRight, - child: IconButton( - icon: const Icon(Icons.close, size: 26), - onPressed: () => Navigator.of(context).pop(), - splashRadius: 22, - tooltip: 'Close', - ), - ), - // Image Viewer - Expanded( - child: PageView.builder( - controller: _controller, - itemCount: widget.imageSources.length, - onPageChanged: (index) { - setState(() => currentIndex = index); - }, - itemBuilder: (context, index) { - final item = widget.imageSources[index]; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: isFile(item) - ? Image.file(item, fit: BoxFit.contain) - : Image.network( - item, - fit: BoxFit.contain, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return Center( - child: CircularProgressIndicator( - value: loadingProgress.expectedTotalBytes != null - ? loadingProgress.cumulativeBytesLoaded / - (loadingProgress.expectedTotalBytes ?? 1) - : null, - ), - ); - }, - errorBuilder: (context, error, stackTrace) => - const Center( - child: Icon(Icons.broken_image, size: 48, color: Colors.grey), - ), + // Add vertical padding to avoid overlap with header/footer + Padding( + padding: + const EdgeInsets.only(top: 72, bottom: 110), // Adjust as needed + child: PageView.builder( + controller: _controller, + itemCount: widget.imageSources.length, + onPageChanged: (index) => setState(() => currentIndex = index), + itemBuilder: (context, index) { + final item = widget.imageSources[index]; + final ImageProvider provider = isFile(item) + ? FileImage(item) + : CachedNetworkImageProvider(item); + + return Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: PhotoView( + imageProvider: provider, + backgroundDecoration: + const BoxDecoration(color: Colors.transparent), + loadingBuilder: (context, event) => + const Center(child: CircularProgressIndicator()), + errorBuilder: (context, error, stackTrace) => + const Center( + child: Icon(Icons.broken_image, + size: 64, color: Colors.white54), + ), + minScale: PhotoViewComputedScale.contained, + maxScale: PhotoViewComputedScale.covered * 3, + ), + ), + ); + }, + ), + ), + + // Top blurred app bar with back button and title + SafeArea( + child: Padding( + padding: const EdgeInsets.all(12), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + height: 50, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.4), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.4), + blurRadius: 8, + offset: Offset(0, 3), + ), + ], + ), + child: Row( + children: [ + Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(24), + onTap: () => Navigator.of(context).pop(), + child: const Padding( + padding: EdgeInsets.all(8), + child: Icon(Icons.arrow_back_ios_new_rounded, + color: Colors.white, size: 24), ), - ); - }, - ), - ), - - // Index Indicator - Padding( - padding: const EdgeInsets.only(top: 8, bottom: 12), - child: Text( - '${currentIndex + 1} / ${widget.imageSources.length}', - style: const TextStyle( - color: Colors.black87, - fontSize: 14, - fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + widget.title ?? 'Preview', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w600, + letterSpacing: 0.5, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), ), ), ), - ], + ), ), - ), + + // Bottom control panel with blur and rounded corners + Align( + alignment: Alignment.bottomCenter, + child: SafeArea( + top: false, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: ClipRRect( + borderRadius: BorderRadius.circular(22), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 14), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.4), + boxShadow: [ + BoxShadow( + color: Colors.black54, + blurRadius: 10, + offset: Offset(0, 4), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Text( + '${currentIndex + 1}/${widget.imageSources.length}', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 15, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + widget.imageSources.length, (index) { + final bool active = index == currentIndex; + return AnimatedContainer( + duration: + const Duration(milliseconds: 300), + margin: const EdgeInsets.symmetric( + horizontal: 4), + height: 8, + width: active ? 22 : 8, + decoration: BoxDecoration( + color: active + ? Colors.white + : Colors.white54, + borderRadius: BorderRadius.circular(30), + boxShadow: active + ? [ + BoxShadow( + color: Colors.white70, + blurRadius: 6, + spreadRadius: 1, + ) + ] + : [], + ), + ); + }), + ), + ), + const SizedBox(width: 16), + Tooltip( + message: 'Download Image', + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(30), + onTap: () async { + final confirmed = await Get.dialog( + ConfirmDialog( + title: "Download Image", + message: + "Do you want to download this image to your device?", + confirmText: "Download", + cancelText: "Cancel", + onConfirm: () async { + // Call your existing download method here for the current image + await downloadImage(widget + .imageSources[currentIndex]); + }, + confirmColor: Colors.blueAccent, + confirmIcon: Icons.download_rounded, + icon: Icons.download_rounded, + errorMessage: + "Failed to download image. Please try again.", + loadingText: "Downloading…", + ), + barrierDismissible: false, + ); + + // Optionally handle if the dialog was cancelled (confirmed == false or null) + if (confirmed != true) { + // Download cancelled by user + } + }, + child: const Padding( + padding: EdgeInsets.all(6), + child: Icon(Icons.download_rounded, + color: Colors.white, size: 28), + ), + ), + ), + ), + ], + ), + if (widget.captions != null && + widget.captions!.isNotEmpty && + currentIndex < widget.captions!.length) + Padding( + padding: const EdgeInsets.only(top: 8), + child: AnimatedOpacity( + opacity: 1.0, + duration: const Duration(milliseconds: 400), + child: Text( + widget.captions![currentIndex], + style: const TextStyle( + color: Colors.white70, + fontSize: 13, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ], ), ); } diff --git a/lib/helpers/widgets/my_confirmation_dialog.dart b/lib/helpers/widgets/my_confirmation_dialog.dart index 338a2fd..fd3ca76 100644 --- a/lib/helpers/widgets/my_confirmation_dialog.dart +++ b/lib/helpers/widgets/my_confirmation_dialog.dart @@ -68,7 +68,7 @@ class ConfirmDialog extends StatelessWidget { maxWidth: 480, ), child: Padding( - padding: const EdgeInsets.fromLTRB(24, 20, 24, 16), + padding: const EdgeInsets.fromLTRB(12, 10, 12, 8), child: _ContentView( title: title, message: message, @@ -158,59 +158,60 @@ class _ContentView extends StatelessWidget { color: colorScheme.outlineVariant.withValues(alpha: 0.6), ), const SizedBox(height: 12), - Align( - alignment: Alignment.center, - child: SizedBox( - width: double.infinity, // allow full available width - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Obx( - () => _DialogButton( - text: cancelText, - icon: cancelIcon, - color: Colors.transparent, - textColor: colorScheme.onSurface, - isFilled: false, - isLoading: false, - onPressed: loading.value ? null : () => Navigator.pop(context, false), + Align( + alignment: Alignment.center, + child: SizedBox( + width: double.infinity, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Obx( + () => _DialogButton( + text: cancelText, + icon: cancelIcon, + color: Colors.transparent, + textColor: colorScheme.onSurface, + isFilled: false, + isLoading: false, + onPressed: loading.value + ? null + : () => Navigator.pop(context, false), + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: Obx( + () => _DialogButton( + text: loading.value ? loadingText : confirmText, + icon: confirmIcon, + color: confirmColor, + textColor: Colors.white, + isFilled: true, + isLoading: loading.value, + onPressed: () async { + try { + loading.value = true; + await onConfirm(); + Navigator.pop(context, true); + } catch (e) { + showAppSnackbar( + title: "Error", + message: errorMessage, + type: SnackbarType.error, + ); + } finally { + loading.value = false; + } + }, + ), + ), + ), + ], ), ), ), - const SizedBox(width: 20), - Expanded( - child: Obx( - () => _DialogButton( - text: loading.value ? loadingText : confirmText, - icon: confirmIcon, - color: confirmColor, - textColor: Colors.white, - isFilled: true, - isLoading: loading.value, - onPressed: () async { - try { - loading.value = true; - await onConfirm(); - Navigator.pop(context, true); - } catch (e) { - showAppSnackbar( - title: "Error", - message: errorMessage, - type: SnackbarType.error, - ); - } finally { - loading.value = false; - } - }, - ), - ), - ), - ], - ), - ), -), - ], ); } diff --git a/pubspec.yaml b/pubspec.yaml index 4a2723e..44f705c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In Android, build-name is used as versionName while build-number used as versionCode. # Read more about Android versioning at https://developer.android.com/studio/publish/versioning # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at +# Document Reference for iOS Versioning at: # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. @@ -32,7 +32,6 @@ dependencies: flutter_localizations: sdk: flutter - # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 @@ -56,7 +55,7 @@ dependencies: appflowy_board: ^0.1.2 syncfusion_flutter_calendar: ^29.1.40 syncfusion_flutter_maps: ^29.1.40 - http: ^1.2.2 + http: ^1.6.0 geolocator: ^14.0.2 permission_handler: ^12.0.1 image: ^4.0.17 @@ -84,7 +83,10 @@ dependencies: timeago: ^3.7.1 cached_network_image: ^3.4.1 + gallery_saver_plus: ^3.2.9 + share_plus: ^12.0.1 timeline_tile: ^2.0.0 + dev_dependencies: flutter_test: sdk: flutter @@ -147,3 +149,6 @@ flutter: # # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package + +dependency_overrides: + http: ^1.6.0