diff --git a/lib/app.dart b/lib/app.dart index fcc83b50e..b8331b90f 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,5 +1,6 @@ // ignore_for_file: use_build_context_synchronously +import 'package:apidash/models/history_meta_model.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; @@ -101,11 +102,62 @@ class _AppState extends ConsumerState<App> with WindowListener { } } -class DashApp extends ConsumerWidget { +class DashApp extends ConsumerStatefulWidget { const DashApp({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState<ConsumerStatefulWidget> createState() => _DashAppState(); +} + +class _DashAppState extends ConsumerState<DashApp> + with WidgetsBindingObserver, WindowListener { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + if (kIsDesktop) { + windowManager.addListener(this); + } + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + if (kIsDesktop) { + windowManager.removeListener(this); + } + super.dispose(); + } + + // Mobile LifeCyclse (Android, IOS) + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + switch (state) { + case AppLifecycleState.paused: + clearHistoryService(); + default: + break; + } + super.didChangeAppLifecycleState(state); + } + + // Desktop Lifecycle (Windows, macOS, Linux) + @override + void onWindowMinimize() { + clearHistoryService(); + } + + Future<void> clearHistoryService() async { + final Map<String, HistoryMetaModel>? historyMetas = + ref.watch(historyMetaStateNotifier); + if ((historyMetas?.length ?? 0) > kCleaHistoryBackgroundThreshold) { + var settingsModel = await getSettingsFromSharedPrefs(); + await HistoryServiceImpl().autoClearHistory(settingsModel: settingsModel); + } + } + + @override + Widget build(BuildContext context) { final isDarkMode = ref.watch(settingsProvider.select((value) => value.isDark)); final workspaceFolderPath = ref diff --git a/lib/consts.dart b/lib/consts.dart index 7eb1ae95f..cac34f47c 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -70,6 +70,7 @@ final kIconRemoveLight = Icon( const kCodePreviewLinesLimit = 500; enum HistoryRetentionPeriod { + fiveSeconds("5 Seconds", Icons.calendar_view_week_rounded), oneWeek("1 Week", Icons.calendar_view_week_rounded), oneMonth("1 Month", Icons.calendar_view_month_rounded), threeMonths("3 Months", Icons.calendar_month_rounded), @@ -482,3 +483,4 @@ const kMsgClearHistory = const kMsgClearHistorySuccess = 'History cleared successfully'; const kMsgClearHistoryError = 'Error clearing history'; const kMsgShareError = "Unable to share"; +const int kCleaHistoryBackgroundThreshold = 10; diff --git a/lib/main.dart b/lib/main.dart index 8b5fab32a..edee291ae 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -47,7 +47,7 @@ Future<bool> initApp( ); debugPrint("openBoxesStatus: $openBoxesStatus"); if (openBoxesStatus) { - await autoClearHistory(settingsModel: settingsModel); + await HistoryServiceImpl().autoClearHistory(settingsModel: settingsModel); } return openBoxesStatus; } catch (e) { diff --git a/lib/services/history_service.dart b/lib/services/history_service.dart index 487b91640..7f6309e66 100644 --- a/lib/services/history_service.dart +++ b/lib/services/history_service.dart @@ -1,42 +1,62 @@ +import 'dart:developer'; + +import 'dart:async'; import 'package:apidash/models/models.dart'; import 'package:apidash/utils/utils.dart'; + import 'hive_services.dart'; -Future<void> autoClearHistory({SettingsModel? settingsModel}) async { - final historyRetentionPeriod = settingsModel?.historyRetentionPeriod; - DateTime? retentionDate = getRetentionDate(historyRetentionPeriod); +abstract class HistoryService { + Future<void> autoClearHistory({required SettingsModel? settingsModel}); +} - if (retentionDate == null) { - return; - } else { - List<String>? historyIds = hiveHandler.getHistoryIds(); - List<String> toRemoveIds = []; +class HistoryServiceImpl implements HistoryService { + @override + Future<void> autoClearHistory({required SettingsModel? settingsModel}) async { + try { + final historyRetentionPeriod = settingsModel?.historyRetentionPeriod; + DateTime? retentionDate = getRetentionDate(historyRetentionPeriod); + if (retentionDate == null) return; - if (historyIds == null || historyIds.isEmpty) { - return; - } + List<String>? historyIds = hiveHandler.getHistoryIds(); + if (historyIds == null || historyIds.isEmpty) return; - for (var historyId in historyIds) { - var jsonModel = hiveHandler.getHistoryMeta(historyId); - if (jsonModel != null) { - var jsonMap = Map<String, Object?>.from(jsonModel); - HistoryMetaModel historyMetaModelFromJson = - HistoryMetaModel.fromJson(jsonMap); - if (historyMetaModelFromJson.timeStamp.isBefore(retentionDate)) { - toRemoveIds.add(historyId); + List<String> toRemoveIds = historyIds.where((historyId) { + var jsonModel = hiveHandler.getHistoryMeta(historyId); + if (jsonModel != null) { + var jsonMap = Map<String, Object?>.from(jsonModel); + HistoryMetaModel historyMetaModelFromJson = + HistoryMetaModel.fromJson(jsonMap); + return historyMetaModelFromJson.timeStamp.isBefore(retentionDate); } + return false; + }).toList(); + + if (toRemoveIds.isEmpty) return; + + int batchSize = calculateOptimalBatchSize(toRemoveIds.length); + final List<List<String>> batches = createBatches(toRemoveIds, batchSize); + + for (var batch in batches) { + await deleteRecordsInBatches(batch); } - } - if (toRemoveIds.isEmpty) { - return; + hiveHandler.setHistoryIds(historyIds..removeWhere(toRemoveIds.contains)); + } catch (e, st) { + log("Error clearing history records", + name: "autoClearHistory", error: e, stackTrace: st); } + } - for (var id in toRemoveIds) { - await hiveHandler.deleteHistoryRequest(id); - hiveHandler.deleteHistoryMeta(id); + static Future<void> deleteRecordsInBatches(List<String> batch) async { + try { + for (var id in batch) { + hiveHandler.deleteHistoryMeta(id); + hiveHandler.deleteHistoryRequest(id); + } + } catch (e, st) { + log("Error deleting records in batches", + name: "deleteRecordsInBatches", error: e, stackTrace: st); } - hiveHandler.setHistoryIds( - historyIds..removeWhere((id) => toRemoveIds.contains(id))); } -} +} \ No newline at end of file diff --git a/lib/utils/history_utils.dart b/lib/utils/history_utils.dart index d1c3dbbc3..d2851c1f4 100644 --- a/lib/utils/history_utils.dart +++ b/lib/utils/history_utils.dart @@ -1,9 +1,17 @@ import 'package:apidash/models/models.dart'; import 'package:apidash/consts.dart'; + import 'convert_utils.dart'; DateTime stripTime(DateTime dateTime) { - return DateTime(dateTime.year, dateTime.month, dateTime.day); + return DateTime( + dateTime.year, + dateTime.month, + dateTime.day, + dateTime.hour, + dateTime.minute, + dateTime.second, + ); } RequestModel getRequestModelFromHistoryModel(HistoryRequestModel model) { @@ -115,11 +123,30 @@ List<HistoryMetaModel> getRequestGroup( return requestGroup; } +int calculateOptimalBatchSize(int totalRecords) { + if (totalRecords < 100) return 50; + if (totalRecords < 500) return 80; + if (totalRecords < 5000) return 100; + return 500; +} + +List<List<String>> createBatches(List<String> items, int batchSize) { + return List.generate( + (items.length / batchSize).ceil(), + (index) => items.sublist( + index * batchSize, + (index * batchSize + batchSize).clamp(0, items.length), + ), + ); +} + DateTime? getRetentionDate(HistoryRetentionPeriod? retentionPeriod) { DateTime now = DateTime.now(); DateTime today = stripTime(now); switch (retentionPeriod) { + case HistoryRetentionPeriod.fiveSeconds: + return today.subtract(const Duration(seconds: 5)); case HistoryRetentionPeriod.oneWeek: return today.subtract(const Duration(days: 7)); case HistoryRetentionPeriod.oneMonth: