Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions lib/src/model/clock/clock_tool_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/clock/chess_clock.dart';
import 'package:lichess_mobile/src/model/clock/clock_tool_preferences.dart';
import 'package:lichess_mobile/src/model/common/service/sound_service.dart';
import 'package:lichess_mobile/src/model/common/time_increment.dart';

Expand All @@ -29,13 +30,11 @@ class ClockToolController extends Notifier<ClockState> {
ClockSide.top: false,
ClockSide.bottom: false,
};
late Duration _emergencyThreshold;

@override
ClockState build() {
const time = Duration(minutes: 10);
const increment = Duration.zero;
_emergencyThreshold = _calculateEmergencyThreshold(time);
const options = ClockOptions(
topTime: time,
bottomTime: time,
Expand Down Expand Up @@ -64,19 +63,20 @@ class ClockToolController extends Notifier<ClockState> {
);
}

// Emergency threshold is set to 10% of the total time
Duration _calculateEmergencyThreshold(Duration initialTime) {
return Duration(milliseconds: (initialTime.inMilliseconds * 0.1).round());
}

void onClockEmergency() {
final activeSide = state.activeSide;
if (activeSide == null) return;
if (_hasPlayedLowTimeSound[activeSide]!) return;
final warning = ref.read(clockToolPreferencesProvider).lowTimeWarning;
final initialTime = activeSide == ClockSide.top
? state.options.topTime
: state.options.bottomTime;
final threshold = warning.threshold(initialTime);
if (threshold == null) return;
final activeSideTime = activeSide == ClockSide.top
? _clock.blackTime.value
: _clock.whiteTime.value;
if (activeSideTime <= _emergencyThreshold) {
if (activeSideTime <= threshold) {
ref.read(soundServiceProvider).play(Sound.lowTime);
_hasPlayedLowTimeSound[activeSide] = true;
}
Expand Down Expand Up @@ -130,7 +130,6 @@ class ClockToolController extends Notifier<ClockState> {

void updateOptions(TimeIncrement timeIncrement) {
final options = ClockOptions.fromTimeIncrement(timeIncrement);
_emergencyThreshold = _calculateEmergencyThreshold(Duration(seconds: timeIncrement.time));
_hasPlayedLowTimeSound[ClockSide.top] = false;
_hasPlayedLowTimeSound[ClockSide.bottom] = false;
_clock.setTimes(blackTime: options.topTime, whiteTime: options.bottomTime);
Expand Down
74 changes: 74 additions & 0 deletions lib/src/model/clock/clock_tool_preferences.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:lichess_mobile/src/model/settings/preferences_storage.dart';

part 'clock_tool_preferences.freezed.dart';
part 'clock_tool_preferences.g.dart';

final clockToolPreferencesProvider = NotifierProvider<ClockToolPreferencesNotifier, ClockToolPrefs>(
ClockToolPreferencesNotifier.new,
);

class ClockToolPreferencesNotifier extends Notifier<ClockToolPrefs>
with PreferencesStorage<ClockToolPrefs> {
@override
@protected
final prefCategory = PrefCategory.clockTool;

@override
@protected
ClockToolPrefs get defaults => ClockToolPrefs.defaults;

@override
ClockToolPrefs fromJson(Map<String, dynamic> json) => ClockToolPrefs.fromJson(json);

@override
ClockToolPrefs build() => fetch();

Future<void> setLowTimeWarning(LowTimeWarning warning) {
return save(state.copyWith(lowTimeWarning: warning));
}
}

enum LowTimeWarning {
off,
tenPercent,
tenSeconds,
twentySeconds,
thirtySeconds;

// TODO: translate
String get label => switch (this) {
.off => 'Off',
.tenPercent => '10%',
.tenSeconds => '10s',
.twentySeconds => '20s',
.thirtySeconds => '30s',
};

/// Returns the warning threshold given the player's [initialTime],
/// or null if the warning is disabled.
Duration? threshold(Duration initialTime) => switch (this) {
.off => null,
.tenPercent =>
initialTime > Duration.zero ? Duration(milliseconds: initialTime.inMilliseconds ~/ 10) : null,
.tenSeconds => const Duration(seconds: 10),
.twentySeconds => const Duration(seconds: 20),
.thirtySeconds => const Duration(seconds: 30),
};
}

@Freezed(fromJson: true, toJson: true)
sealed class ClockToolPrefs with _$ClockToolPrefs implements Serializable {
const ClockToolPrefs._();

const factory ClockToolPrefs({
@JsonKey(unknownEnumValue: LowTimeWarning.tenPercent, defaultValue: LowTimeWarning.tenPercent)
required LowTimeWarning lowTimeWarning,
}) = _ClockToolPrefs;

static const defaults = ClockToolPrefs(lowTimeWarning: LowTimeWarning.tenPercent);

factory ClockToolPrefs.fromJson(Map<String, dynamic> json) => _$ClockToolPrefsFromJson(json);
}
3 changes: 2 additions & 1 deletion lib/src/model/settings/preferences_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ enum PrefCategory {
broadcast('preferences.broadcast'),
engineEvaluation('preferences.engineEvaluation'),
offlineComputerGame('preferences.offlineComputerGame'),
log('preferences.log');
log('preferences.log'),
clockTool('preferences.clockTool');

const PrefCategory(this.storageKey);

Expand Down
131 changes: 131 additions & 0 deletions lib/src/view/clock/clock_settings.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lichess_mobile/src/model/clock/clock_tool_controller.dart';
import 'package:lichess_mobile/src/model/clock/clock_tool_preferences.dart';
import 'package:lichess_mobile/src/model/common/time_increment.dart';
import 'package:lichess_mobile/src/model/settings/general_preferences.dart';
import 'package:lichess_mobile/src/styles/styles.dart';
import 'package:lichess_mobile/src/utils/l10n_context.dart';
import 'package:lichess_mobile/src/view/play/time_control_modal.dart';
import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart';
import 'package:lichess_mobile/src/widgets/settings.dart';

const _iconSize = 38.0;
const _kIconPadding = EdgeInsets.all(10.0);
Expand Down Expand Up @@ -99,6 +103,12 @@ class ClockSettings extends ConsumerWidget {
),
),
),
Expanded(
child: RotatedBox(
quarterTurns: clockOrientation.quarterTurns,
child: _LowTimeWarningButton(iconSize: _iconSize, enabled: buttonsEnabled),
),
),
Expanded(
child: RotatedBox(
quarterTurns: clockOrientation.quarterTurns,
Expand Down Expand Up @@ -133,6 +143,127 @@ class ClockSettings extends ConsumerWidget {
}
}

class _LowTimeWarningButton extends ConsumerWidget {
const _LowTimeWarningButton({required this.iconSize, required this.enabled});

final double iconSize;
final bool enabled;

@override
Widget build(BuildContext context, WidgetRef ref) {
final warning = ref.watch(clockToolPreferencesProvider.select((p) => p.lowTimeWarning));

return IconButton(
padding: _kIconPadding,
iconSize: iconSize,
// TODO: translate
tooltip: 'Low time warning',
onPressed: enabled
? () => showModalBottomSheet<void>(
context: context,
builder: (_) => const _LowTimeWarningModal(),
)
: null,
icon: Icon(warning == .off ? Icons.alarm_off : Icons.alarm_on),
);
}
}

class _LowTimeWarningModal extends ConsumerWidget {
const _LowTimeWarningModal();

static const _horizontalPadding = EdgeInsets.symmetric(horizontal: 16.0);

@override
Widget build(BuildContext context, WidgetRef ref) {
final current = ref.watch(clockToolPreferencesProvider.select((p) => p.lowTimeWarning));
final notifier = ref.read(clockToolPreferencesProvider.notifier);
const options = LowTimeWarning.values;

return BottomSheetScrollableContainer(
padding: const EdgeInsets.symmetric(vertical: 16.0),
children: [
const Padding(
padding: _horizontalPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// TODO: translate
SettingsSectionTitle('Low time warning'),
],
),
),
const SizedBox(height: 4.0),
Card.filled(
margin: Styles.horizontalBodyPadding.add(Styles.sectionBottomPadding),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 16.0),
child: Row(
children: [
for (int i = 0; i < options.length; i++) ...[
Expanded(
child: _LowTimeChip(
label: options[i].label,
selected: current == options[i],
onTap: () {
notifier.setLowTimeWarning(options[i]);
Navigator.of(context).pop();
},
),
),
if (i < options.length - 1) const SizedBox(width: 8),
],
],
),
),
),
],
);
}
}

class _LowTimeChip extends StatelessWidget {
const _LowTimeChip({required this.label, required this.selected, required this.onTap});

final String label;
final bool selected;
final VoidCallback onTap;

@override
Widget build(BuildContext context) {
final colorScheme = ColorScheme.of(context);
return Container(
decoration: BoxDecoration(
color: selected ? colorScheme.secondaryContainer : colorScheme.surfaceContainerLow,
borderRadius: const BorderRadius.all(Radius.circular(5.0)),
border: Border.fromBorderSide(
BorderSide(
color: selected ? colorScheme.secondaryContainer : Theme.of(context).dividerColor,
),
),
),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(5.0)),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: Center(
child: Text(
label,
style: Styles.timeControl
.merge(Styles.bold)
.copyWith(
color: selected ? colorScheme.onPrimaryContainer : colorScheme.onSurfaceVariant,
),
),
),
),
),
);
}
}

class _PlayResumeButton extends ConsumerWidget {
const _PlayResumeButton(this.iconSize);

Expand Down
Loading