From 86dab70841fd3c48b9d3748f861b335e28acc810 Mon Sep 17 00:00:00 2001 From: CoderJava Date: Sun, 26 Nov 2023 19:12:32 +0700 Subject: [PATCH 1/6] feat: Buat widget_loading_center_full_screen.dart Widget tersebut berfungsi untuk menampilkan loading center full screen overlay --- .../widget_loading_center_full_screen.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 lib/feature/presentation/widget/widget_loading_center_full_screen.dart 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(), + ); + } +} From a64202a09e4bd0f3fc3919171f3d16a47a3da3c8 Mon Sep 17 00:00:00 2001 From: CoderJava Date: Sun, 26 Nov 2023 19:16:57 +0700 Subject: [PATCH 2/6] bugfix: Perbaikan task name yang kosong ketika dimasukkan kedalam database lokal Perbaikan task name yang kosong ketika dimasukkan kedalam database lokal pada saat stop timer. Ini disebabkan karena function `doTakeScreenshot` bersifat `async` dan setelah pemanggilan function `doTakeScreenshot` ada kode untuk set `selectedTask` menjadi null dan ini menyebabkan ketika diinsert ke database lokal `selectedTask`-nya menjadi null. Jadi, solusinya adalah simpan variable `selectedTask` kedalam variable `selectedTaskTemp`. --- .../presentation/page/home/home_page.dart | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) 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); From c1acfed4c431a92f21631764ee8a3090af2192dc Mon Sep 17 00:00:00 2001 From: CoderJava Date: Sun, 26 Nov 2023 19:17:54 +0700 Subject: [PATCH 3/6] feat: Buat function reusable `showDialogConfirmation` dan `showDialogMessage` didalam widget_helper.dart --- lib/core/util/widget_helper.dart | 42 ++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) 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()), + ), + ], + ); + }, + ); + } } From 1af0456119f67154cf591dae31a410819c07849a Mon Sep 17 00:00:00 2001 From: CoderJava Date: Sun, 26 Nov 2023 19:20:39 +0700 Subject: [PATCH 4/6] feat: Buat fitur download photo di halaman photo_view_page.dart Fitur download ini dibuat dalam 2 pengkondisian yaitu, jika file photo-nya ada di server dan ada di lokal. Untuk file photo yang ada di server ini untuk fitur download photo yang sudah naik fotonya ke server. Sementara, untuk file photo yang di lokal ini berarti, file photo-nya masih di lokal. --- .../page/photo_view/photo_view_page.dart | 337 ++++++++++++++---- 1 file changed, 260 insertions(+), 77 deletions(-) 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), ), ); } From cd8acb6a40f6f8705d662b3a5d2a5bb1fb754337 Mon Sep 17 00:00:00 2001 From: CoderJava Date: Sun, 26 Nov 2023 19:27:59 +0700 Subject: [PATCH 5/6] feat: Kirimkan flag apakah foto di halaman photo_view_page.dart bisa didownload atau tidak Untuk saat ini, download foto hanya dipakai di halaman sync_page.dart dan report_screenshot_page.dart saja. Dan download foto ini hanya berlaku untuk foto user tersebut dan super admin. --- .../report_screenshot_page.dart | 40 +++++++++---------- .../presentation/page/sync/sync_page.dart | 12 +++++- lib/main.dart | 9 ++++- 3 files changed, 38 insertions(+), 23 deletions(-) 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/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( From 6e8e2238e4af7715b24b2b5d4ded1bde59847478 Mon Sep 17 00:00:00 2001 From: CoderJava Date: Sun, 26 Nov 2023 19:28:16 +0700 Subject: [PATCH 6/6] feat: Update localization bahasa English --- assets/translations/en-US.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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