From ba3218fd48a09b735351ed80f7334e41482d8819 Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Fri, 17 Apr 2026 16:40:06 +0200 Subject: [PATCH 1/3] feat: added local storage methods for appjumpthreshold --- lib/services/db_service/local_storage_service.dart | 12 ++++++++++++ .../db_service/local_storage_service_interface.dart | 2 ++ 2 files changed, 14 insertions(+) diff --git a/lib/services/db_service/local_storage_service.dart b/lib/services/db_service/local_storage_service.dart index 11b4f99..3bb78ff 100644 --- a/lib/services/db_service/local_storage_service.dart +++ b/lib/services/db_service/local_storage_service.dart @@ -3,6 +3,8 @@ import 'dart:convert'; import 'package:doomscroll_stop/services/db_service/local_storage_service_interface.dart'; import 'package:shared_preferences/shared_preferences.dart'; +const _defaultJumpThresholdMs = 60000; + class LocalStorageService implements LocalStorageInterface { final SharedPreferences _sharedPreferences; LocalStorageService(this._sharedPreferences); @@ -18,4 +20,14 @@ class LocalStorageService implements LocalStorageInterface { if (r == null) return {}; return (jsonDecode(r) as Map).cast(); } + + @override + Future saveJumpThresholdMs(int ms) async { + await _sharedPreferences.setInt('jumpThresholdMs', ms); + } + + @override + Future getJumpThresholdMs() async { + return _sharedPreferences.getInt('jumpThresholdMs') ?? _defaultJumpThresholdMs; + } } diff --git a/lib/services/db_service/local_storage_service_interface.dart b/lib/services/db_service/local_storage_service_interface.dart index 35534b2..6097863 100644 --- a/lib/services/db_service/local_storage_service_interface.dart +++ b/lib/services/db_service/local_storage_service_interface.dart @@ -1,4 +1,6 @@ abstract interface class LocalStorageInterface { Future savePreferences(Map appLimits); Future> getPreferences(); + Future saveJumpThresholdMs(int ms); + Future getJumpThresholdMs(); } From b8ba2f333546db595b40c5a0aca5b705a64e1214 Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Fri, 17 Apr 2026 16:40:38 +0200 Subject: [PATCH 2/3] feat: implemented appjumpthreshold provider and ui modal to pick it --- .../preferences/jump_threshold_modal.dart | 128 ++++++++++++++++++ .../preferences/preferences_page.dart | 6 + .../app_jump_threshold_provider.dart | 29 ++++ lib/providers/app_preferences_provider.dart | 5 +- 4 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 lib/features/preferences/jump_threshold_modal.dart create mode 100644 lib/providers/app_jump_threshold_provider.dart diff --git a/lib/features/preferences/jump_threshold_modal.dart b/lib/features/preferences/jump_threshold_modal.dart new file mode 100644 index 0000000..70a287c --- /dev/null +++ b/lib/features/preferences/jump_threshold_modal.dart @@ -0,0 +1,128 @@ +import 'package:doomscroll_stop/providers/app_jump_threshold_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +const _minMinutes = 1; +const _maxMinutes = 30; + +class JumpThresholdModal extends ConsumerStatefulWidget { + const JumpThresholdModal({super.key}); + + static Future show(BuildContext context) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + builder: (_) => const JumpThresholdModal(), + ); + } + + @override + ConsumerState createState() => _JumpThresholdModalState(); +} + +class _JumpThresholdModalState extends ConsumerState { + late int _minutes; + bool _initialized = false; + + @override + Widget build(BuildContext context) { + final thresholdAsync = ref.watch(appJumpThresholdProvider); + + return thresholdAsync.when( + loading: () => const Padding( + padding: EdgeInsets.all(48), + child: Center(child: CircularProgressIndicator()), + ), + error: (e, _) => Padding( + padding: const EdgeInsets.all(24), + child: Text('Error: $e'), + ), + data: (thresholdMs) { + if (!_initialized) { + _minutes = (thresholdMs / 60000).round().clamp(_minMinutes, _maxMinutes); + _initialized = true; + } + + return Padding( + padding: EdgeInsets.only( + left: 24, + right: 24, + top: 24, + bottom: MediaQuery.of(context).viewInsets.bottom + 24, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'App Switch Threshold', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 4), + Text( + 'Minimum time between app switches before it counts as doomscrolling.', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), + fontSize: 13, + ), + ), + const SizedBox(height: 32), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton.filled( + icon: const Icon(Icons.remove), + onPressed: _minutes > _minMinutes + ? () => setState(() => _minutes--) + : null, + ), + const SizedBox(width: 24), + Text( + '$_minutes min', + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 24), + IconButton.filled( + icon: const Icon(Icons.add), + onPressed: _minutes < _maxMinutes + ? () => setState(() => _minutes++) + : null, + ), + ], + ), + const SizedBox(height: 32), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.pop(context), + child: const Text('CANCEL'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: FilledButton( + onPressed: () async { + await ref + .read(appJumpThresholdProvider.notifier) + .setThreshold(_minutes * 60000); + if (context.mounted) Navigator.pop(context); + }, + child: const Text('SAVE'), + ), + ), + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/features/preferences/preferences_page.dart b/lib/features/preferences/preferences_page.dart index 8516f68..7d447a0 100644 --- a/lib/features/preferences/preferences_page.dart +++ b/lib/features/preferences/preferences_page.dart @@ -1,3 +1,4 @@ +import 'package:doomscroll_stop/features/preferences/jump_threshold_modal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:doomscroll_stop/features/preferences/app_selection_sheet.dart'; @@ -33,6 +34,11 @@ class _PreferencesPageState extends ConsumerState { appBar: AppBar( title: const Text('Tracked Apps'), actions: [ + IconButton( + icon: const Icon(Icons.tune), + tooltip: 'Switch threshold', + onPressed: () => JumpThresholdModal.show(context), + ), IconButton( icon: const Icon(Icons.add), onPressed: () { diff --git a/lib/providers/app_jump_threshold_provider.dart b/lib/providers/app_jump_threshold_provider.dart new file mode 100644 index 0000000..0108d39 --- /dev/null +++ b/lib/providers/app_jump_threshold_provider.dart @@ -0,0 +1,29 @@ +import 'package:doomscroll_stop/providers/app_preferences_provider.dart'; +import 'package:doomscroll_stop/providers/doomscroll_background_service_provider.dart'; +import 'package:doomscroll_stop/services/db_service/local_storage_service_interface.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:get_it/get_it.dart'; + +final appJumpThresholdProvider = + AsyncNotifierProvider( + AppJumpThresholdNotifier.new, + ); + +class AppJumpThresholdNotifier extends AsyncNotifier { + @override + Future build() async { + return GetIt.I.get().getJumpThresholdMs(); + } + + Future setThreshold(int ms) async { + await GetIt.I.get().saveJumpThresholdMs(ms); + state = AsyncValue.data(ms); + + final prefs = ref.read(appPreferencesProvider).value; + if (prefs != null && prefs.isNotEmpty) { + final service = ref.read(doomscrollBackgroundServiceProvider.notifier); + await service.stop(); + await service.start(prefs, ms); + } + } +} diff --git a/lib/providers/app_preferences_provider.dart b/lib/providers/app_preferences_provider.dart index cffe575..e2caafc 100644 --- a/lib/providers/app_preferences_provider.dart +++ b/lib/providers/app_preferences_provider.dart @@ -1,3 +1,4 @@ +import 'package:doomscroll_stop/providers/app_jump_threshold_provider.dart'; import 'package:doomscroll_stop/repositories/preferences_repository.dart'; import 'package:doomscroll_stop/services/db_service/local_storage_service_interface.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -5,7 +6,6 @@ import 'package:doomscroll_stop/providers/doomscroll_background_service_provider import 'package:get_it/get_it.dart'; const maxMinutes = 300; -const defaultAppJumpThresholdMs = 30000; class AppPreferencesNotifier extends AsyncNotifier> { @override @@ -39,7 +39,8 @@ class AppPreferencesNotifier extends AsyncNotifier> { await serviceNotifier.stop(); if (currentValue.isNotEmpty) { - await serviceNotifier.start(currentValue, defaultAppJumpThresholdMs); + final thresholdMs = await ref.read(appJumpThresholdProvider.future); + await serviceNotifier.start(currentValue, thresholdMs); } } } From f8e2ac46e7fc676a871e520daefedbbd25e1f202 Mon Sep 17 00:00:00 2001 From: Juampi Q Date: Fri, 17 Apr 2026 16:41:38 +0200 Subject: [PATCH 3/3] test: add app jump threshold variant to tests --- test/providers/app_preferences_provider_test.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/providers/app_preferences_provider_test.dart b/test/providers/app_preferences_provider_test.dart index 172da83..24565d0 100644 --- a/test/providers/app_preferences_provider_test.dart +++ b/test/providers/app_preferences_provider_test.dart @@ -1,3 +1,4 @@ +import 'package:doomscroll_stop/providers/app_jump_threshold_provider.dart'; import 'package:doomscroll_stop/providers/app_preferences_provider.dart'; import 'package:doomscroll_stop/providers/doomscroll_background_service_provider.dart'; import 'package:doomscroll_stop/repositories/preferences_repository.dart'; @@ -148,6 +149,7 @@ void main() { () => mockRepo.getPreferences(), ).thenAnswer((_) async => {'com.app.a': 60}); when(() => mockStorage.savePreferences(any())).thenAnswer((_) async {}); + when(() => mockStorage.getJumpThresholdMs()).thenAnswer((_) async => 60000); when( () => mockService.isServiceRunning(), ).thenAnswer((_) async => false); @@ -163,8 +165,8 @@ void main() { addTearDown(container.dispose); await container.read(appPreferencesProvider.future); - // Ensure background service provider is initialized too await container.read(doomscrollBackgroundServiceProvider.future); + await container.read(appJumpThresholdProvider.future); await container.read(appPreferencesProvider.notifier).saveAndApply(); @@ -173,7 +175,7 @@ void main() { verify( () => mockService.startDetectionService( appTimeLimits: {'com.app.a': 60}, - appJumpThresholdMs: defaultAppJumpThresholdMs, + appJumpThresholdMs: 60000, ), ).called(1); },