diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index a78c7d0..b595079 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -15,7 +15,7 @@ jobs:
- name: Install & set Flutter version
uses: subosito/flutter-action@v2
with:
- flutter-version: '3.10.4'
+ flutter-version: '3.19.6'
channel: 'stable'
- name: Get package
run: flutter pub get
diff --git a/.gitignore b/.gitignore
index 0dc729e..32e68d9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@
.history
.svn/
migrate_working_dir/
+pubspec.lock
# IntelliJ related
*.iml
diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json
index e88ac33..3d0ce7f 100644
--- a/assets/translations/en-US.json
+++ b/assets/translations/en-US.json
@@ -246,5 +246,69 @@
"new_password": "New Password",
"subtitle_new_password": "Please create a new password that you don't use on any other site",
"change": "Change",
- "create_new_password": "Create new password"
+ "create_new_password": "Create new password",
+ "add_manual_track_successfully": "Add manual track successfully",
+ "add_manual_track": "Add Manual Track",
+ "project": "Project",
+ "select_project": "Select project",
+ "task": "Task",
+ "select_task": "Select task",
+ "select_a_project_first": "Select a project first",
+ "no_data_available": "No data available",
+ "please_choose_a_task": "Please choose a task",
+ "start_time": "Start time",
+ "set_start_time": "Set start time",
+ "time": "Time",
+ "date": "Date",
+ "finish_time": "Finish time",
+ "set_finish_time": "Set finish time",
+ "set_start_and_finish_time": "Set start and finish time",
+ "duration": "Duration",
+ "please_set_start_time": "Please set start time",
+ "please_set_finish_time": "Please set finish time",
+ "title_new_update_available": "New update available",
+ "description_new_update_available": "Click here to download latest version",
+ "screenshot_blur": "Screenshot Blur",
+ "description_screenshot_blur_user": "Set your screenshot to blurred.",
+ "this_setting_is_override_by_super_admin": "This setting is overrided by super admin.",
+ "refresh": "Refresh",
+ "invalid_id_or_user_id": "Invalid ID or user ID",
+ "subtitle_screenshot_blur": "Control members blur screenshot for security and privacy.",
+ "enable_all": "Enable all",
+ "disable_all": "Disable all",
+ "member_n": {
+ "zero": "Member",
+ "one": "Member ({})",
+ "many": "Members ({})",
+ "other": "Members ({})"
+ },
+ "user_setting_updated_successfully": "User setting updated successfully",
+ "override": "Override",
+ "description_override_member_blur_screenshot": "It will override the individual setting and member cannot change this configuration.",
+ "start_date": "Start date",
+ "set_start_date": "Set start date",
+ "finish_date": "Finish date",
+ "set_finish_date": "Set finish date",
+ "please_set_start_date": "Please set start date",
+ "please_set_finish_date": "Please set finish date",
+ "finish_date_time_must_be_after_of_start_date_time": "The finish date time must be after the start date time",
+ "reason": "Reason",
+ "why_are_you_adding_manual_track": "e.g. Forgot to start timer",
+ "please_stop_the_timer_if_you_want_to_logout": "Please stop the timer if you want to logout.",
+ "user_registration": "User Registration",
+ "subtitle_user_registration": "Configuring approval workflow for user registration.",
+ "user_registration_workflow_successfully_updated": "User registration workflow successfully updated",
+ "user_registration_workflow": "User Registration Workflow",
+ "auto_approval": "Auto Approval",
+ "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",
+ "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/devtools_options.yaml b/devtools_options.yaml
new file mode 100644
index 0000000..7e7e7f6
--- /dev/null
+++ b/devtools_options.yaml
@@ -0,0 +1 @@
+extensions:
diff --git a/dist/appcast.xml b/dist/appcast.xml
index 590b29e..45c3ff4 100644
--- a/dist/appcast.xml
+++ b/dist/appcast.xml
@@ -5,23 +5,28 @@
en
Dipantau
-
- Version 1.2.2
+ Version 1.6.0
Hotfix
+
Fitur
- - Perbaki flow penggunaan file screenshot ketika start timer.
+ - Khusus super admin, buatkan fitur untuk melihat original dari screenshot yang diblur.
+ - Buat fitur download screenshot.
+
+ Perbaikan
+
+ - Perbaiki nilai task yang tidak tersimpan ke database lokal.
]]>
- 7
- 1.2.2
+ 11
+ 1.6.0
- Mon, 07 Aug 2023 10:00:00 +0700
+ Sun, 26 Nov 2023 22:00:00 +0700
diff --git a/lib/core/network/network_info.dart b/lib/core/network/network_info.dart
index 2223aa1..e004b87 100644
--- a/lib/core/network/network_info.dart
+++ b/lib/core/network/network_info.dart
@@ -12,6 +12,12 @@ class NetworkInfoImpl implements NetworkInfo {
@override
Future get isConnected async {
final connectivityResult = await connectivity.checkConnectivity();
- return connectivityResult != ConnectivityResult.none;
+ if (connectivityResult.contains(ConnectivityResult.none)) {
+ return false;
+ } else if (connectivityResult.isEmpty) {
+ return false;
+ } else {
+ return true;
+ }
}
}
diff --git a/lib/core/util/enum/sign_up_method.dart b/lib/core/util/enum/sign_up_method.dart
new file mode 100644
index 0000000..a3f9934
--- /dev/null
+++ b/lib/core/util/enum/sign_up_method.dart
@@ -0,0 +1,26 @@
+enum SignUpMethod {
+ manual,
+ auto,
+}
+
+extension SignUpMethodExtension on SignUpMethod {
+ String toValue() {
+ switch (this) {
+ case SignUpMethod.manual:
+ return 'manual_approval';
+ case SignUpMethod.auto:
+ return 'auto_approval';
+ default:
+ return '';
+ }
+ }
+
+ static SignUpMethod? parseString(String value) {
+ if (value.contains('manual')) {
+ return SignUpMethod.manual;
+ } else if (value.contains('auto')) {
+ return SignUpMethod.auto;
+ }
+ return null;
+ }
+}
diff --git a/lib/core/util/helper.dart b/lib/core/util/helper.dart
index 78f8d38..a9eab91 100644
--- a/lib/core/util/helper.dart
+++ b/lib/core/util/helper.dart
@@ -101,4 +101,8 @@ class Helper {
return ConstantErrorMessage().failureUnknown;
}
}
+
+ String removeTrailingSlash(String input) {
+ return input.replaceAll(RegExp(r'/+$'), '');
+ }
}
diff --git a/lib/core/util/widget_helper.dart b/lib/core/util/widget_helper.dart
index e754973..bb1e050 100644
--- a/lib/core/util/widget_helper.dart
+++ b/lib/core/util/widget_helper.dart
@@ -1,6 +1,7 @@
import 'dart:io';
import 'dart:math';
+import 'package:dio/dio.dart';
import 'package:dipantau_desktop_client/core/util/enum/global_variable.dart';
import 'package:dipantau_desktop_client/core/util/helper.dart';
import 'package:dipantau_desktop_client/core/util/shared_preferences_manager.dart';
@@ -11,6 +12,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:path_provider/path_provider.dart';
+import 'package:xml/xml.dart';
class WidgetHelper {
void showSnackBar(BuildContext context, String message) {
@@ -168,9 +170,9 @@ class WidgetHelper {
],
),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
- fontStyle: FontStyle.italic,
- fontWeight: FontWeight.w500,
- ),
+ fontStyle: FontStyle.italic,
+ fontWeight: FontWeight.w500,
+ ),
),
],
),
@@ -209,4 +211,67 @@ class WidgetHelper {
return file;
}
+
+ Future isNewUpdateAvailable() async {
+ final response =
+ await Dio().get('https://raw.githubusercontent.com/CoderJava/dipantau-desktop/main/dist/appcast.xml');
+ final data = response.data;
+ final document = XmlDocument.parse(data);
+ final sparkleVersion = document.findAllElements('sparkle:version');
+ if (sparkleVersion.isNotEmpty) {
+ final element = sparkleVersion.first;
+ final versionText = element.innerText;
+ final newVersion = int.tryParse(versionText);
+ if (newVersion != null) {
+ final strBuildNumberLocal = packageInfo.buildNumber;
+ final buildNumberLocal = int.tryParse(strBuildNumberLocal);
+ if (buildNumberLocal != null) {
+ return newVersion > buildNumberLocal;
+ }
+ }
+ }
+ 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/data/datasource/general/general_remote_data_source.dart b/lib/feature/data/datasource/general/general_remote_data_source.dart
new file mode 100644
index 0000000..0a98915
--- /dev/null
+++ b/lib/feature/data/datasource/general/general_remote_data_source.dart
@@ -0,0 +1,36 @@
+import 'package:dio/dio.dart';
+import 'package:dipantau_desktop_client/config/flavor_config.dart';
+import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart';
+
+abstract class GeneralRemoteDataSource {
+ /// Panggil endpoint [host]/api/ping
+ ///
+ /// Throws [DioException] untuk semua error kode
+ late String pathPing;
+
+ Future ping(String baseUrl);
+}
+
+class GeneralRemoteDataSourceImpl implements GeneralRemoteDataSource {
+ final Dio dio;
+
+ GeneralRemoteDataSourceImpl({
+ required this.dio,
+ });
+
+ final baseUrl = FlavorConfig.instance.values.baseUrl;
+
+ @override
+ String pathPing = '';
+
+ @override
+ Future ping(String baseUrl) async {
+ pathPing = '$baseUrl/api/ping';
+ final response = await dio.get(pathPing);
+ if (response.statusCode.toString().startsWith('2')) {
+ return GeneralResponse.fromJson(response.data);
+ } else {
+ throw DioException(requestOptions: RequestOptions(path: pathPing));
+ }
+ }
+}
diff --git a/lib/feature/data/datasource/project/project_remote_data_source.dart b/lib/feature/data/datasource/project/project_remote_data_source.dart
index a853d4b..749a9f7 100644
--- a/lib/feature/data/datasource/project/project_remote_data_source.dart
+++ b/lib/feature/data/datasource/project/project_remote_data_source.dart
@@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
import 'package:dipantau_desktop_client/config/base_url_config.dart';
import 'package:dipantau_desktop_client/config/flavor_config.dart';
import 'package:dipantau_desktop_client/feature/data/model/project/project_response.dart';
+import 'package:dipantau_desktop_client/feature/data/model/project_task/project_task_response.dart';
abstract class ProjectRemoteDataSource {
/// Panggil endpoint [host]/project/user/:id
@@ -12,6 +13,15 @@ abstract class ProjectRemoteDataSource {
late String pathGetProject;
Future getProject(String userId);
+
+ /// Panggil endpoint [host]/project/user/:id/detail
+ /// path parameter
+ /// id - nilai ID user
+ ///
+ /// Throws [DioException] untuk semua error kode
+ late String pathGetProjectTaskByUserId;
+
+ Future getProjectTaskByUserId(String userId);
}
class ProjectRemoteDataSourceImpl implements ProjectRemoteDataSource {
@@ -44,4 +54,25 @@ class ProjectRemoteDataSourceImpl implements ProjectRemoteDataSource {
throw DioException(requestOptions: RequestOptions(path: pathGetProject));
}
}
+
+ @override
+ String pathGetProjectTaskByUserId = '';
+
+ @override
+ Future getProjectTaskByUserId(String userId) async {
+ pathGetProjectTaskByUserId = '$baseUrl/user/$userId/detail';
+ final response = await dio.get(
+ pathGetProjectTaskByUserId,
+ options: Options(
+ headers: {
+ baseUrlConfig.requiredToken: true,
+ },
+ ),
+ );
+ if (response.statusCode.toString().startsWith('2')) {
+ return ProjectTaskResponse.fromJson(response.data);
+ } else {
+ throw DioException(requestOptions: RequestOptions(path: pathGetProjectTaskByUserId));
+ }
+ }
}
diff --git a/lib/feature/data/datasource/setting/setting_remote_data_source.dart b/lib/feature/data/datasource/setting/setting_remote_data_source.dart
index fc7b9ca..044fbad 100644
--- a/lib/feature/data/datasource/setting/setting_remote_data_source.dart
+++ b/lib/feature/data/datasource/setting/setting_remote_data_source.dart
@@ -1,8 +1,11 @@
import 'package:dio/dio.dart';
import 'package:dipantau_desktop_client/config/base_url_config.dart';
import 'package:dipantau_desktop_client/config/flavor_config.dart';
+import 'package:dipantau_desktop_client/feature/data/model/all_user_setting/all_user_setting_response.dart';
import 'package:dipantau_desktop_client/feature/data/model/kv_setting/kv_setting_body.dart';
import 'package:dipantau_desktop_client/feature/data/model/kv_setting/kv_setting_response.dart';
+import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_body.dart';
+import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_response.dart';
abstract class SettingRemoteDataSource {
/// Panggil endpoint [host]/setting/key-value
@@ -18,6 +21,27 @@ abstract class SettingRemoteDataSource {
late String pathSetKvSetting;
Future setKvSetting(KvSettingBody body);
+
+ /// Panggil endpoint [host]/setting/user/all
+ ///
+ /// Throws [DioException] untuk semua error kode
+ late String pathGetAllUserSetting;
+
+ Future getAllUserSetting();
+
+ /// Panggil endpoint [host]/setting/user
+ ///
+ /// Throws [DioException] untuk semua error kode
+ late String pathGetUserSetting;
+
+ Future getUserSetting();
+
+ /// Panggil endpoint [host]/setting/user
+ ///
+ /// Throws [DioException] untuk semua error kode
+ late String pathUpdateUserSetting;
+
+ Future updateUserSetting(UserSettingBody body);
}
class SettingRemoteDataSourceImpl implements SettingRemoteDataSource {
@@ -72,4 +96,76 @@ class SettingRemoteDataSourceImpl implements SettingRemoteDataSource {
throw DioException(requestOptions: RequestOptions(path: pathSetKvSetting));
}
}
+
+ @override
+ String pathGetAllUserSetting = '';
+
+ @override
+ Future getAllUserSetting() async {
+ pathGetAllUserSetting = '$baseUrl/user/all';
+ final response = await dio.get(
+ pathGetAllUserSetting,
+ options: Options(
+ headers: {
+ baseUrlConfig.requiredToken: true,
+ },
+ ),
+ );
+ if (response.statusCode.toString().startsWith('2')) {
+ return AllUserSettingResponse.fromJson(response.data);
+ } else {
+ throw DioException(requestOptions: RequestOptions(path: pathGetAllUserSetting));
+ }
+ }
+
+ @override
+ String pathGetUserSetting = '';
+
+ @override
+ Future getUserSetting() async {
+ pathGetUserSetting = '$baseUrl/user';
+ final response = await dio.get(
+ pathGetUserSetting,
+ options: Options(
+ headers: {
+ baseUrlConfig.requiredToken: true,
+ },
+ ),
+ );
+ if (response.statusCode.toString().startsWith('2')) {
+ return UserSettingResponse.fromJson(response.data);
+ } else {
+ throw DioException(requestOptions: RequestOptions(path: pathGetUserSetting));
+ }
+ }
+
+ @override
+ String pathUpdateUserSetting = '';
+
+ @override
+ Future updateUserSetting(UserSettingBody body) async {
+ pathUpdateUserSetting = '$baseUrl/user';
+ Map? data;
+ if (body.isOverrideBlurScreenshot == null) {
+ data = {
+ 'data': body.data,
+ };
+ } else {
+ data = body.toJson();
+ }
+ final response = await dio.post(
+ pathUpdateUserSetting,
+ data: data,
+ options: Options(
+ headers: {
+ baseUrlConfig.requiredToken: true,
+ },
+ ),
+ );
+ if (response.statusCode.toString().startsWith('2')) {
+ return true;
+ } else {
+ throw DioException(requestOptions: RequestOptions(path: pathUpdateUserSetting));
+ }
+ }
}
diff --git a/lib/feature/data/datasource/track/track_remote_data_source.dart b/lib/feature/data/datasource/track/track_remote_data_source.dart
index f0cc9e0..62b0143 100644
--- a/lib/feature/data/datasource/track/track_remote_data_source.dart
+++ b/lib/feature/data/datasource/track/track_remote_data_source.dart
@@ -5,6 +5,7 @@ import 'package:dipantau_desktop_client/feature/data/model/create_track/bulk_cre
import 'package:dipantau_desktop_client/feature/data/model/create_track/bulk_create_track_image_body.dart';
import 'package:dipantau_desktop_client/feature/data/model/create_track/create_track_body.dart';
import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart';
+import 'package:dipantau_desktop_client/feature/data/model/manual_create_track/manual_create_track_body.dart';
import 'package:dipantau_desktop_client/feature/data/model/track_user/track_user_response.dart';
import 'package:dipantau_desktop_client/feature/data/model/track_user_lite/track_user_lite_response.dart';
@@ -50,6 +51,13 @@ abstract class TrackRemoteDataSource {
late String pathDeleteTrack;
Future deleteTrackUser(int trackId);
+
+ /// Panggil endpoint [host]/track/manual
+ ///
+ /// Throws [DioException] untuk semua error kode
+ late String pathCreateManualTrack;
+
+ Future createManualTrack(ManualCreateTrackBody body);
}
class TrackRemoteDataSourceImpl implements TrackRemoteDataSource {
@@ -222,4 +230,26 @@ class TrackRemoteDataSourceImpl implements TrackRemoteDataSource {
throw DioException(requestOptions: RequestOptions(path: pathDeleteTrack));
}
}
+
+ @override
+ String pathCreateManualTrack = '';
+
+ @override
+ Future createManualTrack(ManualCreateTrackBody body) async {
+ pathCreateManualTrack = '$baseUrl/manual';
+ final response = await dio.post(
+ pathCreateManualTrack,
+ data: body.toJson(),
+ options: Options(
+ headers: {
+ baseUrlConfig.requiredToken: true,
+ },
+ ),
+ );
+ if (response.statusCode.toString().startsWith('2')) {
+ return GeneralResponse.fromJson(response.data);
+ } else {
+ throw DioException(requestOptions: RequestOptions(path: pathCreateManualTrack));
+ }
+ }
}
diff --git a/lib/feature/data/model/all_user_setting/all_user_setting_response.dart b/lib/feature/data/model/all_user_setting/all_user_setting_response.dart
new file mode 100644
index 0000000..e035d3a
--- /dev/null
+++ b/lib/feature/data/model/all_user_setting/all_user_setting_response.dart
@@ -0,0 +1,33 @@
+import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_response.dart';
+import 'package:equatable/equatable.dart';
+import 'package:json_annotation/json_annotation.dart';
+
+part 'all_user_setting_response.g.dart';
+
+@JsonSerializable()
+class AllUserSettingResponse extends Equatable {
+ @JsonKey(name: 'data')
+ final List? data;
+ @JsonKey(name: 'is_override_blur_screenshot')
+ final bool? isOverrideBlurScreenshot;
+
+ AllUserSettingResponse({
+ required this.data,
+ required this.isOverrideBlurScreenshot,
+ });
+
+ factory AllUserSettingResponse.fromJson(Map json) => _$AllUserSettingResponseFromJson(json);
+
+ Map toJson() => _$AllUserSettingResponseToJson(this);
+
+ @override
+ List