diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index c7bf836..3d0ce7f 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -303,5 +303,12 @@ "description_auto_approval": "New user registration are automatically approved after submitting the registration form.", "manual_approval": "Manual Approval", "description_manual_approval": "New user registration are held in a moderation queue and require an super admin to approve them.", - "please_choose_user_registration_workflow": "Please choose user registration workflow" + "please_choose_user_registration_workflow": "Please choose user registration workflow", + "url_screenshot_invalid": "URL screenshot invalid", + "screenshot_name_invalid": "Screenshot name invalid", + "download_directory_invalid": "Download directory invalid", + "something_went_wrong_with_message": "Something went wrong with message {}", + "screenshot_downloaded_successfully": "Screenshot downloaded successfully", + "file_screenshot_doesnt_exists": "File screenshot doesn't exists", + "path_file_screenshot_invalid": "Path file screenshot invalid" } \ No newline at end of file diff --git a/lib/core/util/widget_helper.dart b/lib/core/util/widget_helper.dart index 9e0f204..bb1e050 100644 --- a/lib/core/util/widget_helper.dart +++ b/lib/core/util/widget_helper.dart @@ -232,4 +232,46 @@ class WidgetHelper { } return false; } + + Future showDialogConfirmation( + BuildContext context, + String title, + String content, + List actions, + ) { + return showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text(title), + content: Text(content), + actions: actions, + ); + }, + ); + } + + Future showDialogMessage( + BuildContext context, + String? title, + String message, + ) { + return showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text(title ?? 'info'.tr()), + content: Text(message), + actions: [ + TextButton( + onPressed: () { + context.pop(); + }, + child: Text('dismiss'.tr()), + ), + ], + ); + }, + ); + } } diff --git a/lib/feature/presentation/page/home/home_page.dart b/lib/feature/presentation/page/home/home_page.dart index ba87022..8fb78c9 100644 --- a/lib/feature/presentation/page/home/home_page.dart +++ b/lib/feature/presentation/page/home/home_page.dart @@ -1206,18 +1206,19 @@ class _HomePageState extends State with TrayListener, WindowListener { }); } - void doTakeScreenshot(DateTime? startTime, DateTime? finishTime, {bool isForceStop = false}) async { + Future doTakeScreenshot(DateTime? startTime, DateTime? finishTime, {bool isForceStop = false}) async { + final selectedTaskTemp = selectedTask; var percentActivity = 0.0; if (counterActivity > 0 && countTimerInSeconds > 0) { percentActivity = (counterActivity / countTimerInSeconds) * 100; } counterActivity = 0; - if (selectedProject == null || selectedTask == null) { + if (selectedProject == null || selectedTaskTemp == null) { return; } - final taskId = selectedTask?.id; + final taskId = selectedTaskTemp.id; if (startTime == null || finishTime == null) { return; @@ -1281,8 +1282,12 @@ class _HomePageState extends State with TrayListener, WindowListener { if (listPathStartScreenshots.isNotEmpty) { // hapus file list path start screenshot karena tidak pakai file tersebut // jika file screenshot-nya dapat pas di end time - final filtered = - listPathStartScreenshots.where((element) => element != null && element.isNotEmpty).map((e) => e!).toList(); + final filtered = listPathStartScreenshots + .where((element) { + return element != null && element.isNotEmpty; + }) + .map((e) => e!) + .toList(); for (final element in filtered) { final file = File(element); if (file.existsSync()) { @@ -1346,14 +1351,14 @@ class _HomePageState extends State with TrayListener, WindowListener { final trackEntity = Track( userId: userId, - taskId: taskId!, + taskId: taskId, startDate: formattedStartDateTime, finishDate: formattedFinishDateTime, activity: activity, files: files, duration: durationInSeconds, projectName: selectedProject?.name ?? '', - taskName: selectedTask?.name ?? '', + taskName: selectedTaskTemp.name, ); final trackEntityId = await trackDao.insertTrack(trackEntity); diff --git a/lib/feature/presentation/page/photo_view/photo_view_page.dart b/lib/feature/presentation/page/photo_view/photo_view_page.dart index 33ff34a..ad6b667 100644 --- a/lib/feature/presentation/page/photo_view/photo_view_page.dart +++ b/lib/feature/presentation/page/photo_view/photo_view_page.dart @@ -1,12 +1,16 @@ import 'dart:io'; +import 'package:dio/dio.dart'; import 'package:dipantau_desktop_client/core/util/enum/global_variable.dart'; import 'package:dipantau_desktop_client/core/util/enum/user_role.dart'; import 'package:dipantau_desktop_client/core/util/images.dart'; import 'package:dipantau_desktop_client/core/util/shared_preferences_manager.dart'; +import 'package:dipantau_desktop_client/core/util/widget_helper.dart'; import 'package:dipantau_desktop_client/feature/data/model/track_user/track_user_response.dart'; +import 'package:dipantau_desktop_client/feature/presentation/widget/widget_loading_center_full_screen.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view_gallery.dart'; @@ -14,12 +18,15 @@ class PhotoViewPage extends StatefulWidget { static const routePath = '/photo-view'; static const routeName = 'photo-view'; static const parameterListPhotos = 'list_photos'; + static const parameterIsShowIconDownload = 'is_show_icon_download'; final List? listPhotos; + final bool? isShowIconDownload; PhotoViewPage({ Key? key, required this.listPhotos, + required this.isShowIconDownload, }) : super(key: key); @override @@ -29,11 +36,14 @@ class PhotoViewPage extends StatefulWidget { class _PhotoViewPageState extends State { final pageController = PageController(); final listPhotos = []; + final valueNotifierLoadingDownload = ValueNotifier(false); + final widgetHelper = WidgetHelper(); var indexSelectedPhoto = 0; UserRole? userRole; var isBlurSettingEnabled = false; var isBlurPreviewEnabled = false; + var isShowIconDownload = false; @override void initState() { @@ -44,67 +54,243 @@ class _PhotoViewPageState extends State { } isBlurSettingEnabled = listPhotos.where((element) => (element.urlBlur ?? '').isNotEmpty).isNotEmpty; isBlurPreviewEnabled = isBlurSettingEnabled; + isShowIconDownload = widget.isShowIconDownload ?? false; super.initState(); } @override Widget build(BuildContext context) { - return listPhotos.isEmpty - ? Center( - child: Text('no_data_to_display'.tr()), - ) - : Stack( - children: [ - PhotoViewGallery.builder( - pageController: pageController, - scrollPhysics: const BouncingScrollPhysics(), - builder: (BuildContext context, int index) { - var photo = ''; - if (isBlurPreviewEnabled) { - photo = listPhotos[index].urlBlur ?? ''; - } else { - photo = listPhotos[index].url ?? ''; - } - return photo.startsWith('http') - ? PhotoViewGalleryPageOptions( - imageProvider: NetworkImage(photo), - initialScale: PhotoViewComputedScale.contained, - heroAttributes: PhotoViewHeroAttributes( - tag: photo, - ), - ) - : PhotoViewGalleryPageOptions( - imageProvider: FileImage(File(photo)), - initialScale: PhotoViewComputedScale.contained, - heroAttributes: PhotoViewHeroAttributes( - tag: photo, - ), - ); - }, - loadingBuilder: (context, loadingProgress) { - final cumulativeBytesLoaded = loadingProgress?.cumulativeBytesLoaded ?? 0; - return Center( - child: CircularProgressIndicator( - strokeWidth: 1, - value: loadingProgress?.expectedTotalBytes != null - ? cumulativeBytesLoaded / loadingProgress!.expectedTotalBytes! - : null, - ), - ); - }, - itemCount: listPhotos.length, - onPageChanged: (index) { - setState(() => indexSelectedPhoto = index); - }, - ), - buildWidgetIconClose(), - buildWidgetIconPreviewSetting(), - Align( - alignment: Alignment.bottomCenter, - child: buildWidgetSliderPreviewPhoto(), + return Scaffold( + body: listPhotos.isEmpty + ? Center( + child: Text('no_data_to_display'.tr()), + ) + : Stack( + children: [ + PhotoViewGallery.builder( + pageController: pageController, + scrollPhysics: const BouncingScrollPhysics(), + builder: (BuildContext context, int index) { + var photo = ''; + if (isBlurPreviewEnabled) { + photo = listPhotos[index].urlBlur ?? ''; + } else { + photo = listPhotos[index].url ?? ''; + } + return photo.startsWith('http') + ? PhotoViewGalleryPageOptions( + imageProvider: NetworkImage(photo), + initialScale: PhotoViewComputedScale.contained, + heroAttributes: PhotoViewHeroAttributes( + tag: photo, + ), + ) + : PhotoViewGalleryPageOptions( + imageProvider: FileImage(File(photo)), + initialScale: PhotoViewComputedScale.contained, + heroAttributes: PhotoViewHeroAttributes( + tag: photo, + ), + ); + }, + loadingBuilder: (context, loadingProgress) { + final cumulativeBytesLoaded = loadingProgress?.cumulativeBytesLoaded ?? 0; + return Center( + child: CircularProgressIndicator( + strokeWidth: 1, + value: loadingProgress?.expectedTotalBytes != null + ? cumulativeBytesLoaded / loadingProgress!.expectedTotalBytes! + : null, + ), + ); + }, + itemCount: listPhotos.length, + onPageChanged: (index) { + setState(() => indexSelectedPhoto = index); + }, + ), + buildWidgetIconClose(), + buildWidgetActionTopEnd(), + Align( + alignment: Alignment.bottomCenter, + child: buildWidgetSliderPreviewPhoto(), + ), + buildWidgetLoadingFullScreen(), + ], + ), + ); + } + + Widget buildWidgetLoadingFullScreen() { + return ValueListenableBuilder( + valueListenable: valueNotifierLoadingDownload, + builder: (BuildContext context, bool isShowLoading, _) { + if (isShowLoading) { + return const WidgetLoadingCenterFullScreen(); + } + return Container(); + }, + ); + } + + Widget buildWidgetActionTopEnd() { + return Align( + alignment: Alignment.topRight, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildWidgetIconPreviewSetting(), + buildWidgetIconDownload(), + ], + ), + ); + } + + Widget buildWidgetIconDownload() { + if (!isShowIconDownload) { + return Container(); + } + + return Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.black.withOpacity(.5), + ), + margin: const EdgeInsets.only( + right: 8, + top: 8, + ), + child: IconButton( + onPressed: () async { + final selectedPhoto = listPhotos[indexSelectedPhoto]; + final url = selectedPhoto.url ?? ''; + + final downloadDirectory = await getDownloadsDirectory(); + final pathDownloadDirectory = downloadDirectory?.path; + if ((pathDownloadDirectory == null || pathDownloadDirectory.isEmpty) && mounted) { + widgetHelper.showDialogMessage( + context, + 'info'.tr(), + 'download_directory_invalid'.tr(), + ); + return; + } + + if (url.startsWith('http')) { + // download file dari url dan simpan ke directory download + final splitUrl = url.split('/'); + if (splitUrl.isEmpty && mounted) { + widgetHelper.showDialogMessage( + context, + 'info'.tr(), + 'url_screenshot_invalid'.tr(), + ); + return; + } + + final itemUrl = splitUrl.last; + final splitItemUrl = itemUrl.split('?'); + if (splitItemUrl.isEmpty && mounted) { + widgetHelper.showDialogMessage( + context, + 'info'.tr(), + 'screenshot_name_invalid'.tr(), + ); + return; + } + + valueNotifierLoadingDownload.value = true; + final filename = splitItemUrl.first; + final response = await Dio().get( + url, + options: Options( + responseType: ResponseType.bytes, ), - ], - ); + ); + try { + final fileNameAndPath = '$pathDownloadDirectory/$filename'; + final file = File(fileNameAndPath); + await file.writeAsBytes(response.data); + if (mounted) { + widgetHelper.showSnackBar( + context, + 'screenshot_downloaded_successfully'.tr(), + ); + } + } catch (error) { + if (mounted) { + widgetHelper.showDialogMessage( + context, + 'info'.tr(), + 'something_went_wrong_with_message'.tr( + args: [ + '$error', + ], + ), + ); + } + } finally { + valueNotifierLoadingDownload.value = false; + } + } else { + // copy file dari lokal ke download directory + valueNotifierLoadingDownload.value = true; + final originalFile = File(url); + try { + if (!originalFile.existsSync() && mounted) { + widgetHelper.showDialogMessage( + context, + 'info'.tr(), + 'file_screenshot_doesnt_exists'.tr(), + ); + return; + } + + final splitPathOriginalFile = url.split('/'); + if (splitPathOriginalFile.isEmpty && mounted) { + widgetHelper.showDialogMessage( + context, + 'info'.tr(), + 'path_file_screenshot_invalid'.tr(), + ); + return; + } + + final filename = splitPathOriginalFile.last; + final fileNameAndPath = '$pathDownloadDirectory/$filename'; + final newFile = File(fileNameAndPath); + await originalFile.copy(newFile.path); + if (mounted) { + widgetHelper.showSnackBar( + context, + 'screenshot_downloaded_successfully'.tr(), + ); + } + } catch (error) { + if (mounted) { + widgetHelper.showDialogMessage( + context, + 'info'.tr(), + 'something_went_wrong_with_message'.tr( + args: [ + '$error', + ], + ), + ); + } + } finally { + valueNotifierLoadingDownload.value = false; + } + } + }, + icon: const Icon( + Icons.download, + color: Colors.white, + ), + padding: const EdgeInsets.all(8), + ), + ); } Widget buildWidgetIconPreviewSetting() { @@ -112,29 +298,26 @@ class _PhotoViewPageState extends State { return Container(); } - return Align( - alignment: Alignment.topRight, - child: Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.black.withOpacity(.5), - ), - margin: const EdgeInsets.only( - right: 8, - top: 8, - ), - child: IconButton( - onPressed: () { - setState(() { - isBlurPreviewEnabled = !isBlurPreviewEnabled; - }); - }, - icon: Icon( - isBlurPreviewEnabled ? Icons.visibility_off : Icons.visibility, - color: Colors.white, - ), - padding: const EdgeInsets.all(8), + return Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.black.withOpacity(.5), + ), + margin: const EdgeInsets.only( + right: 8, + top: 8, + ), + child: IconButton( + onPressed: () { + setState(() { + isBlurPreviewEnabled = !isBlurPreviewEnabled; + }); + }, + icon: Icon( + isBlurPreviewEnabled ? Icons.visibility_off : Icons.visibility, + color: Colors.white, ), + padding: const EdgeInsets.all(8), ), ); } diff --git a/lib/feature/presentation/page/report_screenshot/report_screenshot_page.dart b/lib/feature/presentation/page/report_screenshot/report_screenshot_page.dart index 22fc9f1..4df09c9 100644 --- a/lib/feature/presentation/page/report_screenshot/report_screenshot_page.dart +++ b/lib/feature/presentation/page/report_screenshot/report_screenshot_page.dart @@ -615,6 +615,8 @@ class _ReportScreenshotPageState extends State { }) .map((e) => e) .toList(), + PhotoViewPage.parameterIsShowIconDownload: + userId == element.userId?.toString() || userRole == UserRole.superAdmin, }, ); }, @@ -845,27 +847,23 @@ class _ReportScreenshotPageState extends State { return; } - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text('title_delete_track'.tr()), - content: Text('content_delete_track'.tr()), - actions: [ - TextButton( - onPressed: () => context.pop(false), - child: Text('cancel'.tr()), - ), - TextButton( - onPressed: () => context.pop(true), - style: TextButton.styleFrom( - foregroundColor: Colors.red, - ), - child: Text('delete'.tr()), - ), - ], - ); - }, + widgetHelper.showDialogConfirmation( + context, + 'title_delete_track'.tr(), + 'content_delete_track'.tr(), + [ + TextButton( + onPressed: () => context.pop(false), + child: Text('cancel'.tr()), + ), + TextButton( + onPressed: () => context.pop(true), + style: TextButton.styleFrom( + foregroundColor: Colors.red, + ), + child: Text('delete'.tr()), + ), + ], ).then((value) { if (value != null && value) { trackingBloc.add( diff --git a/lib/feature/presentation/page/sync/sync_page.dart b/lib/feature/presentation/page/sync/sync_page.dart index 4ba2d41..0f1bfa9 100644 --- a/lib/feature/presentation/page/sync/sync_page.dart +++ b/lib/feature/presentation/page/sync/sync_page.dart @@ -7,6 +7,7 @@ import 'package:dipantau_desktop_client/core/util/shared_preferences_manager.dar import 'package:dipantau_desktop_client/core/util/string_extension.dart'; import 'package:dipantau_desktop_client/core/util/widget_helper.dart'; import 'package:dipantau_desktop_client/feature/data/model/create_track/bulk_create_track_data_body.dart'; +import 'package:dipantau_desktop_client/feature/data/model/track_user/track_user_response.dart'; import 'package:dipantau_desktop_client/feature/database/entity/track/track.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/sync_manual/sync_manual_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/photo_view/photo_view_page.dart'; @@ -333,7 +334,16 @@ class _SyncPageState extends State { context.pushNamed( PhotoViewPage.routeName, extra: { - PhotoViewPage.parameterListPhotos: listScreenshots.map((e) => e.path).toList(), + PhotoViewPage.parameterListPhotos: listScreenshots.map((e) { + return ItemFileTrackUserResponse( + id: null, + url: e.path, + sizeInByte: 0, + urlBlur: null, + sizeBlurInByte: 0, + ); + }).toList(), + PhotoViewPage.parameterIsShowIconDownload: true, }, ); }, diff --git a/lib/feature/presentation/widget/widget_loading_center_full_screen.dart b/lib/feature/presentation/widget/widget_loading_center_full_screen.dart new file mode 100644 index 0000000..724f4a0 --- /dev/null +++ b/lib/feature/presentation/widget/widget_loading_center_full_screen.dart @@ -0,0 +1,16 @@ +import 'package:dipantau_desktop_client/feature/presentation/widget/widget_custom_circular_progress_indicator.dart'; +import 'package:flutter/material.dart'; + +class WidgetLoadingCenterFullScreen extends StatelessWidget { + const WidgetLoadingCenterFullScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + height: double.infinity, + color: Colors.black.withOpacity(.5), + child: const WidgetCustomCircularProgressIndicator(), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 041758b..9d2def4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -219,7 +219,14 @@ class _MyAppState extends State { final listPhotos = arguments != null && arguments.containsKey(PhotoViewPage.parameterListPhotos) ? arguments[PhotoViewPage.parameterListPhotos] as List? : null; - return PhotoViewPage(listPhotos: listPhotos); + final isShowIconDownload = + arguments != null && arguments.containsKey(PhotoViewPage.parameterIsShowIconDownload) + ? arguments[PhotoViewPage.parameterIsShowIconDownload] as bool? + : null; + return PhotoViewPage( + listPhotos: listPhotos, + isShowIconDownload: isShowIconDownload, + ); }, ), GoRoute(