Skip to content

Commit 3ed1eaf

Browse files
committed
Merge branch 'dev-shady-clockToolLastTimeSave'
2 parents ede46a4 + 6f6ecb9 commit 3ed1eaf

5 files changed

Lines changed: 156 additions & 48 deletions

File tree

lib/src/model/clock/clock_tool_controller.dart

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
44
import 'package:flutter_riverpod/flutter_riverpod.dart';
55
import 'package:freezed_annotation/freezed_annotation.dart';
66
import 'package:lichess_mobile/src/model/clock/chess_clock.dart';
7+
import 'package:lichess_mobile/src/model/clock/clock_tool_preferences.dart';
78
import 'package:lichess_mobile/src/model/common/service/sound_service.dart';
89
import 'package:lichess_mobile/src/model/common/time_increment.dart';
910

@@ -32,26 +33,36 @@ class ClockToolController extends Notifier<ClockState> {
3233
ClockSide.top: false,
3334
ClockSide.bottom: false,
3435
};
35-
late Duration _emergencyThreshold;
36+
final Map<ClockSide, Duration> _emergencyThresholds = {
37+
ClockSide.top: Duration.zero,
38+
ClockSide.bottom: Duration.zero,
39+
};
3640
DateTime? _delayStartedAt;
3741
Duration? _delayDuration;
3842
Duration? _pausedDelayRemaining;
3943
Duration? _activeTurnStartedWithTime;
4044

4145
@override
4246
ClockState build() {
43-
const time = Duration(minutes: 10);
44-
const increment = Duration.zero;
45-
_emergencyThreshold = _calculateEmergencyThreshold(time);
46-
const options = ClockOptions(
47+
// using read is good enough as config change updates in-memory state which re-renders the ui
48+
// and prefs are also saved
49+
final topTimeIncrement = ref.read(clockToolPreferencesProvider).topTimeIncrement;
50+
final topTime = Duration(seconds: topTimeIncrement.time);
51+
final topIncrement = Duration(seconds: topTimeIncrement.increment);
52+
final bottomTimeIncrement = ref.read(clockToolPreferencesProvider).bottomTimeIncrement;
53+
final bottomTime = Duration(seconds: bottomTimeIncrement.time);
54+
final bottomIncrement = Duration(seconds: bottomTimeIncrement.increment);
55+
_emergencyThresholds[ClockSide.top] = _calculateEmergencyThreshold(topTime);
56+
_emergencyThresholds[ClockSide.bottom] = _calculateEmergencyThreshold(bottomTime);
57+
final options = ClockOptions(
4758
type: ClockTimeControlType.increment,
48-
topTime: time,
49-
bottomTime: time,
50-
topIncrement: increment,
51-
bottomIncrement: increment,
59+
topTime: topTime,
60+
bottomTime: bottomTime,
61+
topIncrement: topIncrement,
62+
bottomIncrement: bottomIncrement,
5263
);
5364

54-
_clock = ChessClock(blackTime: time, whiteTime: time, onFlag: _onFlagged);
65+
_clock = ChessClock(blackTime: topTime, whiteTime: bottomTime, onFlag: _onFlagged);
5566

5667
// Add listeners for both clocks
5768
_clock.whiteTime.addListener(onClockEmergency);
@@ -84,7 +95,7 @@ class ClockToolController extends Notifier<ClockState> {
8495
final activeSideTime = activeSide == ClockSide.top
8596
? _clock.blackTime.value
8697
: _clock.whiteTime.value;
87-
if (activeSideTime <= _emergencyThreshold) {
98+
if (activeSideTime <= _emergencyThresholds[activeSide]!) {
8899
ref.read(soundServiceProvider).play(Sound.lowTime);
89100
_hasPlayedLowTimeSound[activeSide] = true;
90101
}
@@ -113,20 +124,11 @@ class ClockToolController extends Notifier<ClockState> {
113124
_startActiveSide(playerType.opposite);
114125
}
115126

116-
void updateDuration(ClockSide playerType, Duration duration) {
117-
if (state.flagged != null || state.paused) {
118-
return;
119-
}
120-
121-
_clock.setTimes(
122-
whiteTime: playerType == ClockSide.bottom ? duration + state.options.topIncrement : null,
123-
blackTime: playerType == ClockSide.top ? duration + state.options.bottomIncrement : null,
124-
);
125-
}
126-
127127
void updateOptions(TimeIncrement timeIncrement) {
128128
final options = ClockOptions.fromTimeIncrement(timeIncrement, type: state.options.type);
129-
_emergencyThreshold = _calculateEmergencyThreshold(Duration(seconds: timeIncrement.time));
129+
final threshold = _calculateEmergencyThreshold(Duration(seconds: timeIncrement.time));
130+
_emergencyThresholds[ClockSide.top] = threshold;
131+
_emergencyThresholds[ClockSide.bottom] = threshold;
130132
_hasPlayedLowTimeSound[ClockSide.top] = false;
131133
_hasPlayedLowTimeSound[ClockSide.bottom] = false;
132134
_clock.setTimes(blackTime: options.topTime, whiteTime: options.bottomTime);
@@ -135,6 +137,7 @@ class ClockToolController extends Notifier<ClockState> {
135137
topTime: _clock.blackTime,
136138
bottomTime: _clock.whiteTime,
137139
);
140+
ref.read(clockToolPreferencesProvider.notifier).setTimeIncrement(timeIncrement);
138141
}
139142

140143
void updateOptionsCustom(TimeIncrement clock, ClockSide player) {
@@ -151,6 +154,8 @@ class ClockToolController extends Notifier<ClockState> {
151154
? Duration(seconds: clock.increment)
152155
: state.options.bottomIncrement,
153156
);
157+
_emergencyThresholds[player] = _calculateEmergencyThreshold(Duration(seconds: clock.time));
158+
_hasPlayedLowTimeSound[player] = false;
154159
_clock.setTimes(blackTime: options.topTime, whiteTime: options.bottomTime);
155160
state = ClockState(
156161
options: options,
@@ -159,6 +164,12 @@ class ClockToolController extends Notifier<ClockState> {
159164
activeSide: state.activeSide,
160165
clockOrientation: state.clockOrientation,
161166
);
167+
168+
if (player == ClockSide.top) {
169+
ref.read(clockToolPreferencesProvider.notifier).setTopTimeIncrement(clock);
170+
} else {
171+
ref.read(clockToolPreferencesProvider.notifier).setBottomTimeIncrement(clock);
172+
}
162173
}
163174

164175
void updateClockType(ClockTimeControlType type) {
@@ -199,14 +210,10 @@ class ClockToolController extends Notifier<ClockState> {
199210
final active = state.activeSide;
200211
// If the active side started at zero, only resume ticking after that side
201212
// has completed at least one move; otherwise behave normally.
202-
final Duration initialOfActive = active == ClockSide.top
203-
? state.options.topTime
204-
: state.options.bottomTime;
205-
final bool hasActiveMoved = active == ClockSide.top
206-
? state.topMoves > 0
207-
: state.bottomMoves > 0;
208-
209-
if (active != null && (initialOfActive.inMilliseconds != 0 || hasActiveMoved)) {
213+
final bool hasActiveMoved = active != null && state.getMovesCount(active) > 0;
214+
215+
if (active != null &&
216+
(state.options.getStartTime(active).inMilliseconds != 0 || hasActiveMoved)) {
210217
final delay = _pausedDelayRemaining;
211218
_markDelay(delay);
212219
_clock.start(delay: delay);
@@ -223,13 +230,8 @@ class ClockToolController extends Notifier<ClockState> {
223230
// Start the countdown only if either this is not a zero-start clock
224231
// or the active side has already made at least one move.
225232
// This makes 0+increment modes usable.
226-
final Duration initialOfActive = activeSide == ClockSide.top
227-
? state.options.topTime
228-
: state.options.bottomTime;
229-
final bool hasActiveMoved = activeSide == ClockSide.top
230-
? state.topMoves > 0
231-
: state.bottomMoves > 0;
232-
if (initialOfActive.inMilliseconds != 0 || hasActiveMoved) {
233+
final bool hasActiveMoved = state.getMovesCount(activeSide) > 0;
234+
if (state.options.getStartTime(activeSide).inMilliseconds != 0 || hasActiveMoved) {
233235
final delay = _delayFor(activeSide);
234236
_markDelay(delay);
235237
_activeTurnStartedWithTime = state.getDuration(activeSide).value;
@@ -334,6 +336,10 @@ sealed class ClockOptions with _$ClockOptions {
334336
bool hasIncrement(ClockSide playerType) {
335337
return getIncrement(playerType) > 0;
336338
}
339+
340+
Duration getStartTime(ClockSide playerType) {
341+
return playerType == ClockSide.top ? topTime : bottomTime;
342+
}
337343
}
338344

339345
@freezed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import 'package:flutter_riverpod/flutter_riverpod.dart';
2+
import 'package:freezed_annotation/freezed_annotation.dart';
3+
import 'package:lichess_mobile/src/model/common/time_increment.dart';
4+
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';
5+
import 'package:lichess_mobile/src/model/user/user.dart';
6+
7+
part 'clock_tool_preferences.freezed.dart';
8+
part 'clock_tool_preferences.g.dart';
9+
10+
final clockToolPreferencesProvider = NotifierProvider<ClockToolPreferences, ClockToolPrefs>(
11+
ClockToolPreferences.new,
12+
name: 'ClockToolPreferencesProvider',
13+
);
14+
15+
class ClockToolPreferences extends Notifier<ClockToolPrefs>
16+
with SessionPreferencesStorage<ClockToolPrefs> {
17+
@override
18+
@protected
19+
final prefCategory = PrefCategory.clockTool;
20+
21+
@override
22+
ClockToolPrefs defaults({LightUser? user}) => ClockToolPrefs.defaults;
23+
24+
@override
25+
ClockToolPrefs fromJson(Map<String, dynamic> json) => ClockToolPrefs.fromJson(json);
26+
27+
@override
28+
ClockToolPrefs build() {
29+
return fetch();
30+
}
31+
32+
/// Sets both clocks to the same [timeIncrement] and remembers it as the last
33+
/// global choice (used to seed the global clock settings modal).
34+
Future<void> setTimeIncrement(TimeIncrement timeIncrement) {
35+
return save(
36+
state.copyWith(
37+
timeIncrement: timeIncrement,
38+
topTimeIncrement: timeIncrement,
39+
bottomTimeIncrement: timeIncrement,
40+
),
41+
);
42+
}
43+
44+
Future<void> setTopTimeIncrement(TimeIncrement timeIncrement) {
45+
return save(state.copyWith(topTimeIncrement: timeIncrement));
46+
}
47+
48+
Future<void> setBottomTimeIncrement(TimeIncrement timeIncrement) {
49+
return save(state.copyWith(bottomTimeIncrement: timeIncrement));
50+
}
51+
}
52+
53+
@Freezed(fromJson: true, toJson: true)
54+
sealed class ClockToolPrefs with _$ClockToolPrefs implements Serializable {
55+
const ClockToolPrefs._();
56+
57+
const factory ClockToolPrefs({
58+
/// The last time increment chosen from the global settings modal (which sets
59+
/// both clocks at once). Used to seed that modal's sliders, so that editing a
60+
/// single clock does not change what the global modal shows.
61+
required TimeIncrement timeIncrement,
62+
required TimeIncrement topTimeIncrement,
63+
required TimeIncrement bottomTimeIncrement,
64+
}) = _ClockToolPrefs;
65+
66+
static const defaults = ClockToolPrefs(
67+
timeIncrement: TimeIncrement(600, 0),
68+
topTimeIncrement: TimeIncrement(600, 0),
69+
bottomTimeIncrement: TimeIncrement(600, 0),
70+
);
71+
72+
factory ClockToolPrefs.fromJson(Map<String, dynamic> json) {
73+
try {
74+
return _$ClockToolPrefsFromJson(json);
75+
} catch (_) {
76+
return defaults;
77+
}
78+
}
79+
}

lib/src/model/settings/preferences_storage.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ enum PrefCategory {
3131
broadcast('preferences.broadcast'),
3232
engineEvaluation('preferences.engineEvaluation'),
3333
offlineComputerGame('preferences.offlineComputerGame'),
34-
log('preferences.log');
34+
log('preferences.log'),
35+
clockTool('preferences.clockTool');
3536

3637
const PrefCategory(this.storageKey);
3738

lib/src/view/clock/clock_settings.dart

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_riverpod/flutter_riverpod.dart';
33
import 'package:lichess_mobile/src/model/clock/clock_tool_controller.dart';
4-
import 'package:lichess_mobile/src/model/common/time_increment.dart';
4+
import 'package:lichess_mobile/src/model/clock/clock_tool_preferences.dart';
55
import 'package:lichess_mobile/src/model/settings/general_preferences.dart';
66
import 'package:lichess_mobile/src/utils/l10n_context.dart';
77
import 'package:lichess_mobile/src/view/clock/clock_tool_settings_modal.dart';
@@ -64,15 +64,18 @@ class ClockSettings extends ConsumerWidget {
6464
isScrollControlled: true,
6565
constraints: BoxConstraints(maxHeight: screenHeight - (screenHeight / 10)),
6666
builder: (BuildContext context) {
67-
final options = ref.watch(
68-
clockToolControllerProvider.select((value) => value.options),
67+
final clockType = ref.watch(
68+
clockToolControllerProvider.select((value) => value.options.type),
69+
);
70+
// Seed from the last global choice, not the current per-side
71+
// times, so editing a single clock does not change what this
72+
// modal shows.
73+
final timeIncrement = ref.watch(
74+
clockToolPreferencesProvider.select((prefs) => prefs.timeIncrement),
6975
);
7076
return ClockToolSettingsModal(
71-
clockType: options.type,
72-
timeIncrement: TimeIncrement(
73-
options.bottomTime.inSeconds,
74-
options.bottomIncrement.inSeconds,
75-
),
77+
clockType: clockType,
78+
timeIncrement: timeIncrement,
7679
onClockTypeSelected: (type) {
7780
ref.read(clockToolControllerProvider.notifier).updateClockType(type);
7881
},

test/model/clock/clock_tool_controller_test.dart

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,34 @@ import 'package:fake_async/fake_async.dart';
22
import 'package:flutter_riverpod/flutter_riverpod.dart';
33
import 'package:flutter_test/flutter_test.dart';
44
import 'package:lichess_mobile/src/model/clock/clock_tool_controller.dart';
5+
import 'package:lichess_mobile/src/model/clock/clock_tool_preferences.dart';
56
import 'package:lichess_mobile/src/model/common/service/sound_service.dart';
67
import 'package:lichess_mobile/src/model/common/time_increment.dart';
78

89
import '../common/service/fake_sound_service.dart';
910

11+
class FakeClockToolPreferences extends ClockToolPreferences {
12+
@override
13+
ClockToolPrefs build() => ClockToolPrefs.defaults;
14+
15+
@override
16+
Future<void> setTimeIncrement(TimeIncrement timeIncrement) async {
17+
await Future<void>.delayed(Duration.zero);
18+
state = state.copyWith(
19+
timeIncrement: timeIncrement,
20+
topTimeIncrement: timeIncrement,
21+
bottomTimeIncrement: timeIncrement,
22+
);
23+
}
24+
}
25+
1026
void main() {
1127
ProviderContainer makeClockContainer() {
1228
final container = ProviderContainer(
13-
overrides: [soundServiceProvider.overrideWithValue(FakeSoundService())],
29+
overrides: [
30+
soundServiceProvider.overrideWithValue(FakeSoundService()),
31+
clockToolPreferencesProvider.overrideWith(FakeClockToolPreferences.new),
32+
],
1433
);
1534
final subscription = container.listen(clockToolControllerProvider, (_, _) {});
1635
addTearDown(subscription.close);

0 commit comments

Comments
 (0)